windows-exe-decompiler-mcp-server 0.1.0
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/CODEX_INSTALLATION.md +69 -0
- package/COPILOT_INSTALLATION.md +77 -0
- package/LICENSE +21 -0
- package/README.md +314 -0
- package/bin/windows-exe-decompiler-mcp-server.js +3 -0
- package/dist/analysis-provenance.d.ts +184 -0
- package/dist/analysis-provenance.js +74 -0
- package/dist/analysis-task-runner.d.ts +31 -0
- package/dist/analysis-task-runner.js +160 -0
- package/dist/artifact-inventory.d.ts +23 -0
- package/dist/artifact-inventory.js +175 -0
- package/dist/cache-manager.d.ts +128 -0
- package/dist/cache-manager.js +454 -0
- package/dist/confidence-semantics.d.ts +66 -0
- package/dist/confidence-semantics.js +122 -0
- package/dist/config.d.ts +335 -0
- package/dist/config.js +193 -0
- package/dist/database.d.ts +227 -0
- package/dist/database.js +601 -0
- package/dist/decompiler-worker.d.ts +441 -0
- package/dist/decompiler-worker.js +1962 -0
- package/dist/dynamic-trace.d.ts +95 -0
- package/dist/dynamic-trace.js +629 -0
- package/dist/env-validator.d.ts +15 -0
- package/dist/env-validator.js +249 -0
- package/dist/error-handler.d.ts +28 -0
- package/dist/error-handler.example.d.ts +22 -0
- package/dist/error-handler.example.js +141 -0
- package/dist/error-handler.js +139 -0
- package/dist/ghidra-analysis-status.d.ts +49 -0
- package/dist/ghidra-analysis-status.js +178 -0
- package/dist/ghidra-config.d.ts +134 -0
- package/dist/ghidra-config.js +464 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +200 -0
- package/dist/job-queue.d.ts +169 -0
- package/dist/job-queue.js +407 -0
- package/dist/logger.d.ts +106 -0
- package/dist/logger.js +176 -0
- package/dist/policy-guard.d.ts +115 -0
- package/dist/policy-guard.js +243 -0
- package/dist/process-output.d.ts +15 -0
- package/dist/process-output.js +90 -0
- package/dist/prompts/function-explanation-review.d.ts +5 -0
- package/dist/prompts/function-explanation-review.js +64 -0
- package/dist/prompts/semantic-name-review.d.ts +5 -0
- package/dist/prompts/semantic-name-review.js +63 -0
- package/dist/runtime-correlation.d.ts +34 -0
- package/dist/runtime-correlation.js +279 -0
- package/dist/runtime-paths.d.ts +3 -0
- package/dist/runtime-paths.js +11 -0
- package/dist/selection-diff.d.ts +667 -0
- package/dist/selection-diff.js +53 -0
- package/dist/semantic-name-suggestion-artifacts.d.ts +116 -0
- package/dist/semantic-name-suggestion-artifacts.js +314 -0
- package/dist/server.d.ts +129 -0
- package/dist/server.js +578 -0
- package/dist/tools/artifact-read.d.ts +235 -0
- package/dist/tools/artifact-read.js +317 -0
- package/dist/tools/artifacts-diff.d.ts +728 -0
- package/dist/tools/artifacts-diff.js +304 -0
- package/dist/tools/artifacts-list.d.ts +515 -0
- package/dist/tools/artifacts-list.js +389 -0
- package/dist/tools/attack-map.d.ts +290 -0
- package/dist/tools/attack-map.js +519 -0
- package/dist/tools/cache-observability.d.ts +4 -0
- package/dist/tools/cache-observability.js +36 -0
- package/dist/tools/code-function-cfg.d.ts +50 -0
- package/dist/tools/code-function-cfg.js +102 -0
- package/dist/tools/code-function-decompile.d.ts +55 -0
- package/dist/tools/code-function-decompile.js +103 -0
- package/dist/tools/code-function-disassemble.d.ts +43 -0
- package/dist/tools/code-function-disassemble.js +185 -0
- package/dist/tools/code-function-explain-apply.d.ts +255 -0
- package/dist/tools/code-function-explain-apply.js +225 -0
- package/dist/tools/code-function-explain-prepare.d.ts +535 -0
- package/dist/tools/code-function-explain-prepare.js +276 -0
- package/dist/tools/code-function-explain-review.d.ts +397 -0
- package/dist/tools/code-function-explain-review.js +589 -0
- package/dist/tools/code-function-rename-apply.d.ts +248 -0
- package/dist/tools/code-function-rename-apply.js +220 -0
- package/dist/tools/code-function-rename-prepare.d.ts +506 -0
- package/dist/tools/code-function-rename-prepare.js +279 -0
- package/dist/tools/code-function-rename-review.d.ts +574 -0
- package/dist/tools/code-function-rename-review.js +761 -0
- package/dist/tools/code-functions-list.d.ts +37 -0
- package/dist/tools/code-functions-list.js +91 -0
- package/dist/tools/code-functions-rank.d.ts +34 -0
- package/dist/tools/code-functions-rank.js +90 -0
- package/dist/tools/code-functions-reconstruct.d.ts +2725 -0
- package/dist/tools/code-functions-reconstruct.js +2807 -0
- package/dist/tools/code-functions-search.d.ts +39 -0
- package/dist/tools/code-functions-search.js +90 -0
- package/dist/tools/code-reconstruct-export.d.ts +1212 -0
- package/dist/tools/code-reconstruct-export.js +4002 -0
- package/dist/tools/code-reconstruct-plan.d.ts +274 -0
- package/dist/tools/code-reconstruct-plan.js +342 -0
- package/dist/tools/dotnet-metadata-extract.d.ts +541 -0
- package/dist/tools/dotnet-metadata-extract.js +355 -0
- package/dist/tools/dotnet-reconstruct-export.d.ts +567 -0
- package/dist/tools/dotnet-reconstruct-export.js +1151 -0
- package/dist/tools/dotnet-types-list.d.ts +325 -0
- package/dist/tools/dotnet-types-list.js +201 -0
- package/dist/tools/dynamic-dependencies.d.ts +115 -0
- package/dist/tools/dynamic-dependencies.js +213 -0
- package/dist/tools/dynamic-memory-import.d.ts +10 -0
- package/dist/tools/dynamic-memory-import.js +567 -0
- package/dist/tools/dynamic-trace-import.d.ts +10 -0
- package/dist/tools/dynamic-trace-import.js +235 -0
- package/dist/tools/entrypoint-fallback-disasm.d.ts +30 -0
- package/dist/tools/entrypoint-fallback-disasm.js +89 -0
- package/dist/tools/ghidra-analyze.d.ts +88 -0
- package/dist/tools/ghidra-analyze.js +208 -0
- package/dist/tools/ghidra-health.d.ts +37 -0
- package/dist/tools/ghidra-health.js +212 -0
- package/dist/tools/ioc-export.d.ts +209 -0
- package/dist/tools/ioc-export.js +542 -0
- package/dist/tools/packer-detect.d.ts +165 -0
- package/dist/tools/packer-detect.js +284 -0
- package/dist/tools/pe-exports-extract.d.ts +175 -0
- package/dist/tools/pe-exports-extract.js +253 -0
- package/dist/tools/pe-fingerprint.d.ts +234 -0
- package/dist/tools/pe-fingerprint.js +269 -0
- package/dist/tools/pe-imports-extract.d.ts +105 -0
- package/dist/tools/pe-imports-extract.js +245 -0
- package/dist/tools/report-generate.d.ts +157 -0
- package/dist/tools/report-generate.js +457 -0
- package/dist/tools/report-summarize.d.ts +2131 -0
- package/dist/tools/report-summarize.js +596 -0
- package/dist/tools/runtime-detect.d.ts +135 -0
- package/dist/tools/runtime-detect.js +247 -0
- package/dist/tools/sample-ingest.d.ts +94 -0
- package/dist/tools/sample-ingest.js +327 -0
- package/dist/tools/sample-profile-get.d.ts +183 -0
- package/dist/tools/sample-profile-get.js +121 -0
- package/dist/tools/sandbox-execute.d.ts +441 -0
- package/dist/tools/sandbox-execute.js +392 -0
- package/dist/tools/strings-extract.d.ts +375 -0
- package/dist/tools/strings-extract.js +314 -0
- package/dist/tools/strings-floss-decode.d.ts +143 -0
- package/dist/tools/strings-floss-decode.js +259 -0
- package/dist/tools/system-health.d.ts +434 -0
- package/dist/tools/system-health.js +446 -0
- package/dist/tools/task-cancel.d.ts +21 -0
- package/dist/tools/task-cancel.js +70 -0
- package/dist/tools/task-status.d.ts +27 -0
- package/dist/tools/task-status.js +106 -0
- package/dist/tools/task-sweep.d.ts +22 -0
- package/dist/tools/task-sweep.js +77 -0
- package/dist/tools/tool-help.d.ts +340 -0
- package/dist/tools/tool-help.js +261 -0
- package/dist/tools/yara-scan.d.ts +554 -0
- package/dist/tools/yara-scan.js +313 -0
- package/dist/types.d.ts +266 -0
- package/dist/types.js +41 -0
- package/dist/worker-pool.d.ts +204 -0
- package/dist/worker-pool.js +650 -0
- package/dist/workflows/deep-static.d.ts +104 -0
- package/dist/workflows/deep-static.js +276 -0
- package/dist/workflows/function-explanation-review.d.ts +655 -0
- package/dist/workflows/function-explanation-review.js +440 -0
- package/dist/workflows/reconstruct.d.ts +2053 -0
- package/dist/workflows/reconstruct.js +666 -0
- package/dist/workflows/semantic-name-review.d.ts +2418 -0
- package/dist/workflows/semantic-name-review.js +521 -0
- package/dist/workflows/triage.d.ts +659 -0
- package/dist/workflows/triage.js +1374 -0
- package/dist/workspace-manager.d.ts +150 -0
- package/dist/workspace-manager.js +411 -0
- package/ghidra_scripts/DecompileFunction.java +487 -0
- package/ghidra_scripts/DecompileFunction.py +150 -0
- package/ghidra_scripts/ExtractCFG.java +256 -0
- package/ghidra_scripts/ExtractCFG.py +233 -0
- package/ghidra_scripts/ExtractFunctions.java +442 -0
- package/ghidra_scripts/ExtractFunctions.py +101 -0
- package/ghidra_scripts/README.md +125 -0
- package/ghidra_scripts/SearchFunctionReferences.java +380 -0
- package/helpers/DotNetMetadataProbe/DotNetMetadataProbe.csproj +9 -0
- package/helpers/DotNetMetadataProbe/Program.cs +566 -0
- package/install-to-codex.ps1 +178 -0
- package/install-to-copilot.ps1 +303 -0
- package/package.json +101 -0
- package/requirements.txt +9 -0
- package/workers/requirements-dynamic.txt +11 -0
- package/workers/requirements.txt +8 -0
- package/workers/speakeasy_compat.py +175 -0
- package/workers/static_worker.py +5183 -0
- package/workers/yara_rules/default.yar +33 -0
- package/workers/yara_rules/malware_families.yar +93 -0
- package/workers/yara_rules/packers.yar +80 -0
|
@@ -0,0 +1,1374 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Triage workflow implementation
|
|
3
|
+
* Quick threat assessment workflow that completes within 5 minutes
|
|
4
|
+
* Requirements: 15.1, 15.2, 15.4, 15.5
|
|
5
|
+
*/
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { createPEFingerprintHandler } from '../tools/pe-fingerprint.js';
|
|
8
|
+
import { createRuntimeDetectHandler } from '../tools/runtime-detect.js';
|
|
9
|
+
import { createPEImportsExtractHandler } from '../tools/pe-imports-extract.js';
|
|
10
|
+
import { createStringsExtractHandler } from '../tools/strings-extract.js';
|
|
11
|
+
import { createYaraScanHandler } from '../tools/yara-scan.js';
|
|
12
|
+
// ============================================================================
|
|
13
|
+
// Constants
|
|
14
|
+
// ============================================================================
|
|
15
|
+
const TOOL_NAME = 'workflow.triage';
|
|
16
|
+
// Suspicious API patterns for IOC detection
|
|
17
|
+
const HIGH_RISK_APIS = [
|
|
18
|
+
'CreateRemoteThread',
|
|
19
|
+
'VirtualAllocEx',
|
|
20
|
+
'WriteProcessMemory',
|
|
21
|
+
'SetWindowsHookEx',
|
|
22
|
+
];
|
|
23
|
+
const CONTEXT_DEPENDENT_APIS = [
|
|
24
|
+
'GetAsyncKeyState',
|
|
25
|
+
'InternetOpen',
|
|
26
|
+
'InternetConnect',
|
|
27
|
+
'HttpSendRequest',
|
|
28
|
+
'URLDownloadToFile',
|
|
29
|
+
'WinExec',
|
|
30
|
+
'ShellExecute',
|
|
31
|
+
'CreateProcess',
|
|
32
|
+
'RegSetValue',
|
|
33
|
+
'RegCreateKey',
|
|
34
|
+
'CryptEncrypt',
|
|
35
|
+
'CryptDecrypt',
|
|
36
|
+
];
|
|
37
|
+
const SUSPICIOUS_APIS = [...new Set([...HIGH_RISK_APIS, ...CONTEXT_DEPENDENT_APIS])];
|
|
38
|
+
const LibraryProfileSchema = z.object({
|
|
39
|
+
ecosystems: z.array(z.string()),
|
|
40
|
+
top_crates: z.array(z.string()),
|
|
41
|
+
notable_libraries: z.array(z.string()),
|
|
42
|
+
evidence: z.array(z.string()),
|
|
43
|
+
});
|
|
44
|
+
const NOTABLE_LIBRARY_HINTS = [
|
|
45
|
+
{ name: 'tokio', patterns: [/\btokio\b/i] },
|
|
46
|
+
{ name: 'goblin', patterns: [/\bgoblin\b/i] },
|
|
47
|
+
{ name: 'iced-x86', patterns: [/\biced[-_]?x86\b/i] },
|
|
48
|
+
{ name: 'clap', patterns: [/\bclap\b/i] },
|
|
49
|
+
{ name: 'sysinfo', patterns: [/\bsysinfo\b/i] },
|
|
50
|
+
{ name: 'reqwest', patterns: [/\breqwest\b/i] },
|
|
51
|
+
{ name: 'serde', patterns: [/\bserde\b/i] },
|
|
52
|
+
{ name: 'mio', patterns: [/\bmio\b/i] },
|
|
53
|
+
{ name: 'pelite', patterns: [/\bpelite\b/i] },
|
|
54
|
+
{ name: 'object', patterns: [/\bobject\b/i] },
|
|
55
|
+
{ name: 'winapi', patterns: [/\bwinapi\b/i] },
|
|
56
|
+
{ name: 'ntapi', patterns: [/\bntapi\b/i] },
|
|
57
|
+
{ name: 'windows-sys', patterns: [/\bwindows[-_]?sys\b/i] },
|
|
58
|
+
];
|
|
59
|
+
// ============================================================================
|
|
60
|
+
// Input/Output Schemas
|
|
61
|
+
// ============================================================================
|
|
62
|
+
/**
|
|
63
|
+
* Input schema for triage workflow
|
|
64
|
+
* Requirements: 15.1
|
|
65
|
+
*/
|
|
66
|
+
export const TriageWorkflowInputSchema = z.object({
|
|
67
|
+
sample_id: z.string().describe('Sample ID (format: sha256:<hex>)'),
|
|
68
|
+
force_refresh: z
|
|
69
|
+
.boolean()
|
|
70
|
+
.optional()
|
|
71
|
+
.default(false)
|
|
72
|
+
.describe('Bypass cache in dependent static tools'),
|
|
73
|
+
});
|
|
74
|
+
/**
|
|
75
|
+
* IOC (Indicators of Compromise) structure
|
|
76
|
+
* Requirements: 15.2, 15.5
|
|
77
|
+
*/
|
|
78
|
+
const IOCSchema = z.object({
|
|
79
|
+
suspicious_imports: z.array(z.string()).describe('Suspicious imported functions'),
|
|
80
|
+
suspicious_strings: z.array(z.string()).describe('Suspicious strings found'),
|
|
81
|
+
yara_matches: z.array(z.string()).describe('YARA rule matches'),
|
|
82
|
+
yara_low_confidence: z.array(z.string()).optional().describe('YARA matches downgraded due to weak evidence'),
|
|
83
|
+
urls: z.array(z.string()).optional().describe('URLs found in strings'),
|
|
84
|
+
ip_addresses: z.array(z.string()).optional().describe('IP addresses found'),
|
|
85
|
+
file_paths: z.array(z.string()).optional().describe('File paths found'),
|
|
86
|
+
registry_keys: z.array(z.string()).optional().describe('Registry keys found'),
|
|
87
|
+
high_value_iocs: z
|
|
88
|
+
.object({
|
|
89
|
+
suspicious_apis: z.array(z.string()).optional(),
|
|
90
|
+
commands: z.array(z.string()).optional(),
|
|
91
|
+
pipes: z.array(z.string()).optional(),
|
|
92
|
+
urls: z.array(z.string()).optional(),
|
|
93
|
+
network: z.array(z.string()).optional(),
|
|
94
|
+
})
|
|
95
|
+
.optional()
|
|
96
|
+
.describe('Layered high-value IOC view'),
|
|
97
|
+
compiler_artifacts: z
|
|
98
|
+
.object({
|
|
99
|
+
cargo_paths: z.array(z.string()).optional(),
|
|
100
|
+
rust_markers: z.array(z.string()).optional(),
|
|
101
|
+
library_profile: LibraryProfileSchema.optional(),
|
|
102
|
+
})
|
|
103
|
+
.optional()
|
|
104
|
+
.describe('Build/toolchain breadcrumbs separated from high-risk IOC signals'),
|
|
105
|
+
});
|
|
106
|
+
const IntentAssessmentSchema = z.object({
|
|
107
|
+
label: z.enum(['dual_use_tool', 'operator_utility', 'malware_like_payload', 'unknown']),
|
|
108
|
+
confidence: z.number().min(0).max(1),
|
|
109
|
+
evidence: z.array(z.string()),
|
|
110
|
+
counter_evidence: z.array(z.string()),
|
|
111
|
+
});
|
|
112
|
+
const ToolingAssessmentSchema = z.object({
|
|
113
|
+
help_text_detected: z.boolean(),
|
|
114
|
+
cli_surface_detected: z.boolean(),
|
|
115
|
+
framework_hints: z.array(z.string()),
|
|
116
|
+
toolchain_markers: z.array(z.string()),
|
|
117
|
+
library_profile: LibraryProfileSchema.optional(),
|
|
118
|
+
});
|
|
119
|
+
/**
|
|
120
|
+
* Output schema for triage workflow
|
|
121
|
+
* Requirements: 15.2, 15.4, 15.5
|
|
122
|
+
*/
|
|
123
|
+
export const TriageWorkflowOutputSchema = z.object({
|
|
124
|
+
ok: z.boolean(),
|
|
125
|
+
data: z.object({
|
|
126
|
+
summary: z.string().describe('Natural language summary of the analysis'),
|
|
127
|
+
confidence: z.number().min(0).max(1).describe('Confidence score (0-1)'),
|
|
128
|
+
threat_level: z.enum(['clean', 'suspicious', 'malicious', 'unknown']).describe('Assessed threat level'),
|
|
129
|
+
iocs: IOCSchema.describe('Indicators of Compromise'),
|
|
130
|
+
evidence: z.array(z.string()).describe('Evidence supporting the assessment'),
|
|
131
|
+
evidence_weights: z
|
|
132
|
+
.object({
|
|
133
|
+
import: z.number().min(0).max(1),
|
|
134
|
+
string: z.number().min(0).max(1),
|
|
135
|
+
runtime: z.number().min(0).max(1),
|
|
136
|
+
})
|
|
137
|
+
.describe('Relative evidence contribution weights for this conclusion'),
|
|
138
|
+
inference: z
|
|
139
|
+
.object({
|
|
140
|
+
classification: z.enum(['benign', 'suspicious', 'malicious', 'unknown']),
|
|
141
|
+
hypotheses: z.array(z.string()),
|
|
142
|
+
false_positive_risks: z.array(z.string()),
|
|
143
|
+
intent_assessment: IntentAssessmentSchema.optional(),
|
|
144
|
+
tooling_assessment: ToolingAssessmentSchema.optional(),
|
|
145
|
+
})
|
|
146
|
+
.optional()
|
|
147
|
+
.describe('Inference layer derived from evidence, separated for auditability'),
|
|
148
|
+
recommendation: z.string().describe('Recommended next steps'),
|
|
149
|
+
raw_results: z.object({
|
|
150
|
+
fingerprint: z.any().optional(),
|
|
151
|
+
runtime: z.any().optional(),
|
|
152
|
+
imports: z.any().optional(),
|
|
153
|
+
strings: z.any().optional(),
|
|
154
|
+
yara: z.any().optional(),
|
|
155
|
+
}).describe('Raw results from individual tools'),
|
|
156
|
+
}).optional(),
|
|
157
|
+
warnings: z.array(z.string()).optional(),
|
|
158
|
+
errors: z.array(z.string()).optional(),
|
|
159
|
+
metrics: z.object({
|
|
160
|
+
elapsed_ms: z.number(),
|
|
161
|
+
tool: z.string(),
|
|
162
|
+
}).optional(),
|
|
163
|
+
});
|
|
164
|
+
// ============================================================================
|
|
165
|
+
// Tool Definition
|
|
166
|
+
// ============================================================================
|
|
167
|
+
/**
|
|
168
|
+
* Tool definition for triage workflow
|
|
169
|
+
*/
|
|
170
|
+
export const triageWorkflowToolDefinition = {
|
|
171
|
+
name: TOOL_NAME,
|
|
172
|
+
description: '快速画像工作流:在 5 分钟内完成基础威胁评估,包括 PE 指纹、运行时检测、导入表分析、字符串提取和 YARA 扫描',
|
|
173
|
+
inputSchema: TriageWorkflowInputSchema,
|
|
174
|
+
outputSchema: TriageWorkflowOutputSchema,
|
|
175
|
+
};
|
|
176
|
+
// ============================================================================
|
|
177
|
+
// Helper Functions
|
|
178
|
+
// ============================================================================
|
|
179
|
+
/**
|
|
180
|
+
* Analyze imports for suspicious APIs
|
|
181
|
+
* Requirements: 15.5
|
|
182
|
+
*/
|
|
183
|
+
function analyzeSuspiciousImports(imports) {
|
|
184
|
+
const suspicious = [];
|
|
185
|
+
try {
|
|
186
|
+
for (const [dll, functions] of Object.entries(imports)) {
|
|
187
|
+
// Ensure functions is an array
|
|
188
|
+
if (!Array.isArray(functions)) {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
for (const func of functions) {
|
|
192
|
+
if (typeof func === 'string' && SUSPICIOUS_APIS.some(api => func.toLowerCase().includes(api.toLowerCase()))) {
|
|
193
|
+
suspicious.push(`${dll}!${func}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
// Silently handle errors to prevent workflow failure
|
|
200
|
+
console.error('Error analyzing suspicious imports:', error);
|
|
201
|
+
}
|
|
202
|
+
return suspicious;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Analyze strings for suspicious patterns
|
|
206
|
+
* Requirements: 15.5
|
|
207
|
+
*/
|
|
208
|
+
function normalizeStringEntry(entry) {
|
|
209
|
+
if (typeof entry === 'string') {
|
|
210
|
+
return entry;
|
|
211
|
+
}
|
|
212
|
+
if (entry &&
|
|
213
|
+
typeof entry === 'object' &&
|
|
214
|
+
'string' in entry &&
|
|
215
|
+
typeof entry.string === 'string') {
|
|
216
|
+
return entry.string;
|
|
217
|
+
}
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
export function extractCrateNameFromCargoPath(input) {
|
|
221
|
+
const normalized = input.replace(/\//g, '\\');
|
|
222
|
+
const match = normalized.match(/cargo\\(?:registry\\src|git\\checkouts)\\[^\\]+\\([^\\]+)(?:\\|$)/i);
|
|
223
|
+
if (!match?.[1]) {
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
const rawCrate = match[1].trim();
|
|
227
|
+
if (!rawCrate) {
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
const versionMatch = rawCrate.match(/^(.*)-\d[\w.+-]*$/);
|
|
231
|
+
const crateName = versionMatch?.[1] || rawCrate;
|
|
232
|
+
return crateName.trim() || null;
|
|
233
|
+
}
|
|
234
|
+
function detectLibraryHints(str) {
|
|
235
|
+
return NOTABLE_LIBRARY_HINTS
|
|
236
|
+
.filter((hint) => hint.patterns.some((pattern) => pattern.test(str)))
|
|
237
|
+
.map((hint) => hint.name);
|
|
238
|
+
}
|
|
239
|
+
function analyzeSuspiciousStrings(strings) {
|
|
240
|
+
const suspicious = [];
|
|
241
|
+
const urls = [];
|
|
242
|
+
const ips = [];
|
|
243
|
+
const paths = [];
|
|
244
|
+
const registry = [];
|
|
245
|
+
const commands = [];
|
|
246
|
+
const pipes = [];
|
|
247
|
+
const cargoPaths = [];
|
|
248
|
+
const rustMarkers = [];
|
|
249
|
+
const crateNames = [];
|
|
250
|
+
const libraryHints = [];
|
|
251
|
+
for (const rawEntry of strings) {
|
|
252
|
+
const str = normalizeStringEntry(rawEntry);
|
|
253
|
+
if (!str) {
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
// Check for URLs
|
|
257
|
+
const urlMatch = str.match(/https?:\/\/[^\s]+/i);
|
|
258
|
+
if (urlMatch) {
|
|
259
|
+
urls.push(urlMatch[0]);
|
|
260
|
+
suspicious.push(str);
|
|
261
|
+
}
|
|
262
|
+
// Check for IP addresses
|
|
263
|
+
const ipMatch = str.match(/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/);
|
|
264
|
+
if (ipMatch) {
|
|
265
|
+
ips.push(ipMatch[0]);
|
|
266
|
+
suspicious.push(str);
|
|
267
|
+
}
|
|
268
|
+
// Check for file paths
|
|
269
|
+
const pathMatch = str.match(/[A-Za-z]:\\[^\s]+/);
|
|
270
|
+
if (pathMatch) {
|
|
271
|
+
paths.push(pathMatch[0]);
|
|
272
|
+
if (pathMatch[0].toLowerCase().includes('temp') ||
|
|
273
|
+
pathMatch[0].toLowerCase().includes('appdata')) {
|
|
274
|
+
suspicious.push(str);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
// Check for registry keys
|
|
278
|
+
const regMatch = str.match(/HKEY_[A-Z_]+\\[^\s]+/i);
|
|
279
|
+
if (regMatch) {
|
|
280
|
+
registry.push(regMatch[0]);
|
|
281
|
+
suspicious.push(str);
|
|
282
|
+
}
|
|
283
|
+
// Check for shell executables
|
|
284
|
+
if (/cmd\.exe|powershell\.exe|wscript\.exe/i.test(str)) {
|
|
285
|
+
suspicious.push(str);
|
|
286
|
+
commands.push(str);
|
|
287
|
+
}
|
|
288
|
+
// Check for named pipes / IPC
|
|
289
|
+
const pipeMatch = str.match(/\\\\\.\\pipe\\[^\s]+|\\\\pipe\\[^\s]+/i);
|
|
290
|
+
if (pipeMatch) {
|
|
291
|
+
pipes.push(pipeMatch[0]);
|
|
292
|
+
suspicious.push(str);
|
|
293
|
+
}
|
|
294
|
+
// Split compiler/toolchain artifacts from high-value IOC
|
|
295
|
+
const cargoMatch = str.match(/cargo\\registry\\src\\[^\s]+/i);
|
|
296
|
+
if (cargoMatch) {
|
|
297
|
+
cargoPaths.push(cargoMatch[0]);
|
|
298
|
+
const crateName = extractCrateNameFromCargoPath(cargoMatch[0]);
|
|
299
|
+
if (crateName) {
|
|
300
|
+
crateNames.push(crateName);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
if (/rust_panic|core::panicking|\\src\\main\.rs|\\src\\lib\.rs/i.test(str)) {
|
|
304
|
+
rustMarkers.push(str);
|
|
305
|
+
}
|
|
306
|
+
libraryHints.push(...detectLibraryHints(str));
|
|
307
|
+
}
|
|
308
|
+
return {
|
|
309
|
+
suspicious: [...new Set(suspicious)], // Remove duplicates
|
|
310
|
+
urls: [...new Set(urls)],
|
|
311
|
+
ips: [...new Set(ips)],
|
|
312
|
+
paths: [...new Set(paths)],
|
|
313
|
+
registry: [...new Set(registry)],
|
|
314
|
+
commands: [...new Set(commands)],
|
|
315
|
+
pipes: [...new Set(pipes)],
|
|
316
|
+
cargoPaths: [...new Set(cargoPaths)],
|
|
317
|
+
rustMarkers: [...new Set(rustMarkers)],
|
|
318
|
+
crateNames: [...new Set(crateNames)],
|
|
319
|
+
libraryHints: [...new Set(libraryHints)],
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
function normalizeEcosystemLabel(value) {
|
|
323
|
+
const lowered = value.toLowerCase();
|
|
324
|
+
if (lowered.includes('rust')) {
|
|
325
|
+
return 'rust';
|
|
326
|
+
}
|
|
327
|
+
if (lowered.includes('dotnet') || lowered.includes('.net') || lowered.includes('clr')) {
|
|
328
|
+
return '.net';
|
|
329
|
+
}
|
|
330
|
+
if (lowered.includes('go')) {
|
|
331
|
+
return 'go';
|
|
332
|
+
}
|
|
333
|
+
if (lowered.includes('native') || lowered.includes('c++') || lowered.includes('pe')) {
|
|
334
|
+
return 'native';
|
|
335
|
+
}
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
export function buildLibraryProfile(stringAnalysis, runtime) {
|
|
339
|
+
const ecosystems = new Set();
|
|
340
|
+
const crateCounts = new Map();
|
|
341
|
+
for (const runtimeHint of Array.isArray(runtime?.suspected) ? runtime.suspected : []) {
|
|
342
|
+
const runtimeName = typeof runtimeHint?.runtime === 'string' ? runtimeHint.runtime : '';
|
|
343
|
+
const ecosystem = normalizeEcosystemLabel(runtimeName);
|
|
344
|
+
if (ecosystem) {
|
|
345
|
+
ecosystems.add(ecosystem);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
if (stringAnalysis.cargoPaths.length > 0 ||
|
|
349
|
+
stringAnalysis.rustMarkers.length > 0 ||
|
|
350
|
+
stringAnalysis.crateNames.length > 0) {
|
|
351
|
+
ecosystems.add('rust');
|
|
352
|
+
}
|
|
353
|
+
for (const crateName of [...stringAnalysis.crateNames, ...stringAnalysis.libraryHints]) {
|
|
354
|
+
const normalized = crateName.trim().toLowerCase();
|
|
355
|
+
if (!normalized) {
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
crateCounts.set(normalized, (crateCounts.get(normalized) || 0) + 1);
|
|
359
|
+
}
|
|
360
|
+
const rankedCrates = Array.from(crateCounts.entries())
|
|
361
|
+
.sort((left, right) => {
|
|
362
|
+
if (right[1] !== left[1]) {
|
|
363
|
+
return right[1] - left[1];
|
|
364
|
+
}
|
|
365
|
+
return left[0].localeCompare(right[0]);
|
|
366
|
+
})
|
|
367
|
+
.map(([crate]) => crate);
|
|
368
|
+
const topCrates = rankedCrates.slice(0, 8);
|
|
369
|
+
const notableLibraries = Array.from(new Set(rankedCrates.filter((crate) => NOTABLE_LIBRARY_HINTS.some((hint) => hint.name.toLowerCase() === crate)))).slice(0, 8);
|
|
370
|
+
const evidence = [];
|
|
371
|
+
if (topCrates.length > 0) {
|
|
372
|
+
evidence.push(`Cargo/library references observed: ${topCrates.slice(0, 5).join(', ')}`);
|
|
373
|
+
}
|
|
374
|
+
if (stringAnalysis.cargoPaths.length > 0) {
|
|
375
|
+
evidence.push(`Cargo registry paths observed: ${stringAnalysis.cargoPaths.slice(0, 2).join(' | ')}`);
|
|
376
|
+
}
|
|
377
|
+
if (stringAnalysis.rustMarkers.length > 0) {
|
|
378
|
+
evidence.push(`Rust toolchain markers observed: ${stringAnalysis.rustMarkers.slice(0, 2).join(' | ')}`);
|
|
379
|
+
}
|
|
380
|
+
if (ecosystems.size > 0) {
|
|
381
|
+
evidence.push(`Ecosystem hints: ${Array.from(ecosystems).join(', ')}`);
|
|
382
|
+
}
|
|
383
|
+
if (ecosystems.size === 0 && topCrates.length === 0 && notableLibraries.length === 0) {
|
|
384
|
+
return undefined;
|
|
385
|
+
}
|
|
386
|
+
return {
|
|
387
|
+
ecosystems: Array.from(ecosystems),
|
|
388
|
+
top_crates: topCrates,
|
|
389
|
+
notable_libraries: notableLibraries,
|
|
390
|
+
evidence: Array.from(new Set(evidence)),
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
function summarizeLibraryProfile(profile) {
|
|
394
|
+
if (!profile) {
|
|
395
|
+
return '';
|
|
396
|
+
}
|
|
397
|
+
const libraries = profile.notable_libraries.length > 0
|
|
398
|
+
? profile.notable_libraries
|
|
399
|
+
: profile.top_crates;
|
|
400
|
+
if (libraries.length === 0) {
|
|
401
|
+
return profile.ecosystems.join(', ');
|
|
402
|
+
}
|
|
403
|
+
return libraries.slice(0, 3).join(' + ');
|
|
404
|
+
}
|
|
405
|
+
function downgradeYaraLevel(level) {
|
|
406
|
+
if (level === 'high') {
|
|
407
|
+
return 'medium';
|
|
408
|
+
}
|
|
409
|
+
if (level === 'medium') {
|
|
410
|
+
return 'low';
|
|
411
|
+
}
|
|
412
|
+
return 'low';
|
|
413
|
+
}
|
|
414
|
+
function normalizeYaraSignals(matches) {
|
|
415
|
+
if (!Array.isArray(matches)) {
|
|
416
|
+
return [];
|
|
417
|
+
}
|
|
418
|
+
return matches
|
|
419
|
+
.map((match) => {
|
|
420
|
+
const rule = typeof match?.rule === 'string'
|
|
421
|
+
? String(match.rule)
|
|
422
|
+
: '';
|
|
423
|
+
if (!rule) {
|
|
424
|
+
return null;
|
|
425
|
+
}
|
|
426
|
+
const levelRaw = typeof match?.confidence?.level === 'string'
|
|
427
|
+
? String(match.confidence?.level).toLowerCase()
|
|
428
|
+
: 'unknown';
|
|
429
|
+
const level = levelRaw === 'high' || levelRaw === 'medium' || levelRaw === 'low'
|
|
430
|
+
? levelRaw
|
|
431
|
+
: 'unknown';
|
|
432
|
+
const numericScore = Number(match?.confidence?.score || 0);
|
|
433
|
+
const stringOnly = Boolean(match?.evidence?.string_only);
|
|
434
|
+
const loweredRule = rule.toLowerCase();
|
|
435
|
+
return {
|
|
436
|
+
rule,
|
|
437
|
+
level,
|
|
438
|
+
score: Number.isFinite(numericScore) ? numericScore : 0,
|
|
439
|
+
stringOnly,
|
|
440
|
+
generic: loweredRule.includes('generic') ||
|
|
441
|
+
(loweredRule.includes('trojan') && !loweredRule.includes('downloader') && !loweredRule.includes('backdoor')),
|
|
442
|
+
};
|
|
443
|
+
})
|
|
444
|
+
.filter((item) => Boolean(item));
|
|
445
|
+
}
|
|
446
|
+
function assessIntentAndTooling(stringsSummary, suspiciousImports, stringAnalysis, yaraSignals, runtime) {
|
|
447
|
+
const contextWindows = Array.isArray(stringsSummary?.context_windows)
|
|
448
|
+
? stringsSummary.context_windows
|
|
449
|
+
: [];
|
|
450
|
+
const windowTexts = contextWindows.map((window) => Array.isArray(window?.strings)
|
|
451
|
+
? window.strings
|
|
452
|
+
.map((entry) => String(entry?.string || ''))
|
|
453
|
+
.filter((item) => item.length > 0)
|
|
454
|
+
.join('\n')
|
|
455
|
+
: '');
|
|
456
|
+
const joinedWindows = windowTexts.join('\n').toLowerCase();
|
|
457
|
+
const helpTextDetected = /usage:|options?:|examples?:|--help\b|-h\b|commands?:|syntax:/.test(joinedWindows);
|
|
458
|
+
const cliSurfaceDetected = /--[a-z0-9_-]+|-[a-z0-9]\b/.test(joinedWindows) ||
|
|
459
|
+
/\b(pid|process|thread|target|list|inject|suspend|resume|kill|dump)\b/.test(joinedWindows);
|
|
460
|
+
const importApis = suspiciousImports.map((item) => (item.split('!').pop() || item).toLowerCase());
|
|
461
|
+
const processOpsCount = importApis.filter((api) => [
|
|
462
|
+
'openprocess',
|
|
463
|
+
'writeprocessmemory',
|
|
464
|
+
'createremotethread',
|
|
465
|
+
'virtualallocex',
|
|
466
|
+
'suspendthread',
|
|
467
|
+
'resumethread',
|
|
468
|
+
'terminatethread',
|
|
469
|
+
'terminateprocess',
|
|
470
|
+
].some((needle) => api.includes(needle))).length;
|
|
471
|
+
const malwareSpecificYara = yaraSignals.some((signal) => /ransomware|backdoor|downloader|keylogger/.test(signal.rule.toLowerCase()) &&
|
|
472
|
+
signal.level !== 'low');
|
|
473
|
+
const networkBehavior = stringAnalysis.urls.length > 0 || stringAnalysis.ips.length > 0;
|
|
474
|
+
const persistenceBehavior = stringAnalysis.registry.length > 0;
|
|
475
|
+
const suspectedRuntimes = Array.isArray(runtime?.suspected)
|
|
476
|
+
? runtime.suspected
|
|
477
|
+
.map((item) => String(item?.runtime || '').trim())
|
|
478
|
+
.filter((item) => item.length > 0)
|
|
479
|
+
: [];
|
|
480
|
+
const toolchainMarkers = [
|
|
481
|
+
...(Array.isArray(runtime?.suspected)
|
|
482
|
+
? runtime.suspected
|
|
483
|
+
.map((item) => `${String(item?.runtime || '').trim()}${typeof item?.confidence === 'number' ? `(${item.confidence.toFixed(2)})` : ''}`)
|
|
484
|
+
.filter((item) => item.length > 0)
|
|
485
|
+
: []),
|
|
486
|
+
...stringAnalysis.cargoPaths.slice(0, 5),
|
|
487
|
+
...stringAnalysis.rustMarkers.slice(0, 5),
|
|
488
|
+
];
|
|
489
|
+
const libraryProfile = buildLibraryProfile(stringAnalysis, runtime);
|
|
490
|
+
const frameworkHints = Array.from(new Set([
|
|
491
|
+
...suspectedRuntimes,
|
|
492
|
+
...(libraryProfile?.ecosystems || []),
|
|
493
|
+
...(libraryProfile?.notable_libraries.slice(0, 3) || []),
|
|
494
|
+
]));
|
|
495
|
+
if (helpTextDetected &&
|
|
496
|
+
cliSurfaceDetected &&
|
|
497
|
+
processOpsCount > 0 &&
|
|
498
|
+
!malwareSpecificYara &&
|
|
499
|
+
!networkBehavior &&
|
|
500
|
+
!persistenceBehavior) {
|
|
501
|
+
return {
|
|
502
|
+
intent: {
|
|
503
|
+
label: 'dual_use_tool',
|
|
504
|
+
confidence: 0.78,
|
|
505
|
+
evidence: [
|
|
506
|
+
'Long-form help/usage text grouped in nearby string windows.',
|
|
507
|
+
'CLI-style options and operator verbs are present.',
|
|
508
|
+
'Process-operation APIs are present without stronger malware-specific corroboration.',
|
|
509
|
+
],
|
|
510
|
+
counter_evidence: [
|
|
511
|
+
'Static evidence alone cannot rule out malicious repurposing of the tool.',
|
|
512
|
+
],
|
|
513
|
+
},
|
|
514
|
+
tooling: {
|
|
515
|
+
help_text_detected: helpTextDetected,
|
|
516
|
+
cli_surface_detected: cliSurfaceDetected,
|
|
517
|
+
framework_hints: frameworkHints,
|
|
518
|
+
toolchain_markers: toolchainMarkers.slice(0, 10),
|
|
519
|
+
library_profile: libraryProfile,
|
|
520
|
+
},
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
if (helpTextDetected || cliSurfaceDetected) {
|
|
524
|
+
return {
|
|
525
|
+
intent: {
|
|
526
|
+
label: 'operator_utility',
|
|
527
|
+
confidence: 0.62,
|
|
528
|
+
evidence: [
|
|
529
|
+
'Operator-facing help or CLI surface detected in grouped string windows.',
|
|
530
|
+
],
|
|
531
|
+
counter_evidence: malwareSpecificYara
|
|
532
|
+
? ['Malware-specific YARA evidence is also present; treat as suspicious until validated.']
|
|
533
|
+
: [],
|
|
534
|
+
},
|
|
535
|
+
tooling: {
|
|
536
|
+
help_text_detected: helpTextDetected,
|
|
537
|
+
cli_surface_detected: cliSurfaceDetected,
|
|
538
|
+
framework_hints: frameworkHints,
|
|
539
|
+
toolchain_markers: toolchainMarkers.slice(0, 10),
|
|
540
|
+
library_profile: libraryProfile,
|
|
541
|
+
},
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
if (malwareSpecificYara) {
|
|
545
|
+
return {
|
|
546
|
+
intent: {
|
|
547
|
+
label: 'malware_like_payload',
|
|
548
|
+
confidence: 0.74,
|
|
549
|
+
evidence: ['Malware-family-like YARA evidence is present.'],
|
|
550
|
+
counter_evidence: [],
|
|
551
|
+
},
|
|
552
|
+
tooling: {
|
|
553
|
+
help_text_detected: helpTextDetected,
|
|
554
|
+
cli_surface_detected: cliSurfaceDetected,
|
|
555
|
+
framework_hints: frameworkHints,
|
|
556
|
+
toolchain_markers: toolchainMarkers.slice(0, 10),
|
|
557
|
+
library_profile: libraryProfile,
|
|
558
|
+
},
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
return {
|
|
562
|
+
intent: {
|
|
563
|
+
label: 'unknown',
|
|
564
|
+
confidence: 0.35,
|
|
565
|
+
evidence: [],
|
|
566
|
+
counter_evidence: [],
|
|
567
|
+
},
|
|
568
|
+
tooling: {
|
|
569
|
+
help_text_detected: helpTextDetected,
|
|
570
|
+
cli_surface_detected: cliSurfaceDetected,
|
|
571
|
+
framework_hints: frameworkHints,
|
|
572
|
+
toolchain_markers: toolchainMarkers.slice(0, 10),
|
|
573
|
+
library_profile: libraryProfile,
|
|
574
|
+
},
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
export function applyIntentAwareYaraAdjustments(yaraSignals, intentAssessment) {
|
|
578
|
+
if (!Array.isArray(yaraSignals) ||
|
|
579
|
+
yaraSignals.length === 0 ||
|
|
580
|
+
!['dual_use_tool', 'operator_utility'].includes(intentAssessment.label) ||
|
|
581
|
+
intentAssessment.confidence < 0.55) {
|
|
582
|
+
return yaraSignals;
|
|
583
|
+
}
|
|
584
|
+
return yaraSignals.map((signal) => {
|
|
585
|
+
if (!signal.generic || hasMalwareSpecificSignal(signal)) {
|
|
586
|
+
return signal;
|
|
587
|
+
}
|
|
588
|
+
const dualUse = intentAssessment.label === 'dual_use_tool';
|
|
589
|
+
const adjustedLevel = dualUse ? 'low' : downgradeYaraLevel(signal.level);
|
|
590
|
+
const adjustedScore = Number((Math.max(0, signal.score) * (dualUse ? 0.45 : 0.7)).toFixed(2));
|
|
591
|
+
return {
|
|
592
|
+
...signal,
|
|
593
|
+
level: adjustedLevel,
|
|
594
|
+
score: adjustedScore,
|
|
595
|
+
};
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* Calculate threat level based on IOCs
|
|
600
|
+
* Requirements: 15.2, 15.4
|
|
601
|
+
*/
|
|
602
|
+
function calculateThreatLevel(yaraMatches, suspiciousImports, suspiciousStrings, lowConfidenceYaraCount = 0) {
|
|
603
|
+
let score = 0;
|
|
604
|
+
let maxScore = 0;
|
|
605
|
+
// YARA matches (highest weight)
|
|
606
|
+
maxScore += 50;
|
|
607
|
+
if (yaraMatches.length > 0) {
|
|
608
|
+
// Check for malware family matches
|
|
609
|
+
const hasMalwareMatch = yaraMatches.some(rule => rule.toLowerCase().includes('trojan') ||
|
|
610
|
+
rule.toLowerCase().includes('ransomware') ||
|
|
611
|
+
rule.toLowerCase().includes('backdoor') ||
|
|
612
|
+
rule.toLowerCase().includes('malware'));
|
|
613
|
+
if (hasMalwareMatch) {
|
|
614
|
+
score += 50;
|
|
615
|
+
}
|
|
616
|
+
else {
|
|
617
|
+
// Packer or other matches
|
|
618
|
+
score += 20;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
else if (lowConfidenceYaraCount > 0) {
|
|
622
|
+
// Weak YARA-only signal: keep as low weight to reduce false positives.
|
|
623
|
+
score += Math.min(lowConfidenceYaraCount * 2, 6);
|
|
624
|
+
}
|
|
625
|
+
// Suspicious imports (context-aware weighting to reduce false positives)
|
|
626
|
+
maxScore += 30;
|
|
627
|
+
if (suspiciousImports.length > 0) {
|
|
628
|
+
const importApis = suspiciousImports
|
|
629
|
+
.map((item) => item.split('!').pop() || item)
|
|
630
|
+
.map((item) => item.toLowerCase());
|
|
631
|
+
const highRiskCount = importApis.filter((name) => HIGH_RISK_APIS.some((api) => name.includes(api.toLowerCase()))).length;
|
|
632
|
+
const contextDependentCount = importApis.filter((name) => CONTEXT_DEPENDENT_APIS.some((api) => name.includes(api.toLowerCase()))).length;
|
|
633
|
+
let importScore = 0;
|
|
634
|
+
// High-risk primitives (injection/hooking) are strong signals.
|
|
635
|
+
importScore += Math.min(highRiskCount * 8, 22);
|
|
636
|
+
// Context-dependent APIs are weaker alone (debuggers/installers use them too).
|
|
637
|
+
importScore += Math.min(contextDependentCount * 2, 8);
|
|
638
|
+
const hasWriteProcessMemory = importApis.some((name) => name.includes('writeprocessmemory'));
|
|
639
|
+
const hasCreateRemoteThread = importApis.some((name) => name.includes('createremotethread'));
|
|
640
|
+
const hasVirtualAllocEx = importApis.some((name) => name.includes('virtualallocex'));
|
|
641
|
+
if (hasWriteProcessMemory && (hasCreateRemoteThread || hasVirtualAllocEx)) {
|
|
642
|
+
importScore += 6;
|
|
643
|
+
}
|
|
644
|
+
score += Math.min(importScore, 30);
|
|
645
|
+
}
|
|
646
|
+
// Suspicious strings (lower weight)
|
|
647
|
+
maxScore += 20;
|
|
648
|
+
if (suspiciousStrings.length > 0) {
|
|
649
|
+
score += Math.min(suspiciousStrings.length * 2, 20);
|
|
650
|
+
}
|
|
651
|
+
// Calculate confidence
|
|
652
|
+
const confidence = maxScore > 0 ? score / maxScore : 0;
|
|
653
|
+
// Determine threat level
|
|
654
|
+
let level;
|
|
655
|
+
if (score >= 40) {
|
|
656
|
+
level = 'malicious';
|
|
657
|
+
}
|
|
658
|
+
else if (score >= 15) {
|
|
659
|
+
level = 'suspicious';
|
|
660
|
+
}
|
|
661
|
+
else if (score > 0) {
|
|
662
|
+
level = 'suspicious';
|
|
663
|
+
}
|
|
664
|
+
else {
|
|
665
|
+
level = 'clean';
|
|
666
|
+
}
|
|
667
|
+
return { level, confidence };
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* Generate evidence list
|
|
671
|
+
* Requirements: 15.2
|
|
672
|
+
*/
|
|
673
|
+
function generateEvidence(yaraMatches, suspiciousImports, suspiciousStrings, runtime) {
|
|
674
|
+
const evidence = [];
|
|
675
|
+
if (yaraMatches.length > 0) {
|
|
676
|
+
evidence.push(`YARA 规则匹配: ${yaraMatches.join(', ')}`);
|
|
677
|
+
}
|
|
678
|
+
if (suspiciousImports.length > 0) {
|
|
679
|
+
evidence.push(`检测到 ${suspiciousImports.length} 个可疑导入函数`);
|
|
680
|
+
if (suspiciousImports.length <= 5) {
|
|
681
|
+
evidence.push(`可疑导入: ${suspiciousImports.join(', ')}`);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
if (suspiciousStrings.length > 0) {
|
|
685
|
+
evidence.push(`检测到 ${suspiciousStrings.length} 个可疑字符串`);
|
|
686
|
+
}
|
|
687
|
+
if (runtime?.is_dotnet) {
|
|
688
|
+
evidence.push(`.NET 程序 (${runtime.dotnet_version || 'unknown version'})`);
|
|
689
|
+
}
|
|
690
|
+
if (runtime?.suspected && runtime.suspected.length > 0) {
|
|
691
|
+
const runtimes = runtime.suspected.map((s) => s.runtime).join(', ');
|
|
692
|
+
evidence.push(`检测到运行时: ${runtimes}`);
|
|
693
|
+
}
|
|
694
|
+
return evidence;
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* Generate summary and recommendation
|
|
698
|
+
* Requirements: 15.2
|
|
699
|
+
*/
|
|
700
|
+
function generateSummaryAndRecommendation(threatLevel, yaraMatches, runtime) {
|
|
701
|
+
let summary = '';
|
|
702
|
+
let recommendation = '';
|
|
703
|
+
// Generate summary based on threat level
|
|
704
|
+
if (threatLevel === 'malicious') {
|
|
705
|
+
const malwareTypes = yaraMatches
|
|
706
|
+
.filter(rule => rule.toLowerCase().includes('trojan') ||
|
|
707
|
+
rule.toLowerCase().includes('ransomware') ||
|
|
708
|
+
rule.toLowerCase().includes('backdoor'))
|
|
709
|
+
.map(rule => rule.split('_')[0]);
|
|
710
|
+
if (malwareTypes.length > 0) {
|
|
711
|
+
summary = `检测到恶意软件: ${malwareTypes.join(', ')}`;
|
|
712
|
+
}
|
|
713
|
+
else {
|
|
714
|
+
summary = '检测到高度可疑的恶意行为特征';
|
|
715
|
+
}
|
|
716
|
+
recommendation = '强烈建议在隔离环境中进行深度分析,不要在生产环境执行此文件';
|
|
717
|
+
}
|
|
718
|
+
else if (threatLevel === 'suspicious') {
|
|
719
|
+
const packerMatches = yaraMatches.filter(rule => rule.toLowerCase().includes('upx') ||
|
|
720
|
+
rule.toLowerCase().includes('packer') ||
|
|
721
|
+
rule.toLowerCase().includes('themida') ||
|
|
722
|
+
rule.toLowerCase().includes('vmprotect'));
|
|
723
|
+
if (packerMatches.length > 0) {
|
|
724
|
+
summary = `检测到加壳器: ${packerMatches.join(', ')}`;
|
|
725
|
+
recommendation = '建议进行脱壳分析或深度静态分析以了解真实行为';
|
|
726
|
+
}
|
|
727
|
+
else {
|
|
728
|
+
summary = '检测到可疑行为特征,需要进一步分析';
|
|
729
|
+
recommendation = '建议进行深度静态分析或在隔离环境中进行动态分析';
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
else if (threatLevel === 'clean') {
|
|
733
|
+
summary = '未检测到明显的恶意行为特征';
|
|
734
|
+
recommendation = '样本看起来相对安全,但建议根据具体使用场景进行进一步验证';
|
|
735
|
+
}
|
|
736
|
+
else {
|
|
737
|
+
summary = '无法确定威胁等级,需要更多信息';
|
|
738
|
+
recommendation = '建议进行深度静态分析以获取更多信息';
|
|
739
|
+
}
|
|
740
|
+
// Add runtime info to summary
|
|
741
|
+
if (runtime?.is_dotnet) {
|
|
742
|
+
summary += ` (.NET 程序)`;
|
|
743
|
+
}
|
|
744
|
+
return { summary, recommendation };
|
|
745
|
+
}
|
|
746
|
+
function calculateEvidenceWeights(suspiciousImports, suspiciousStrings, runtime, yaraMatches, yaraLowConfidenceMatches) {
|
|
747
|
+
let importWeight = Math.min(0.9, suspiciousImports.length * 0.06);
|
|
748
|
+
let stringWeight = Math.min(0.8, suspiciousStrings.length * 0.03) +
|
|
749
|
+
Math.min(0.35, yaraMatches.length * 0.09 + yaraLowConfidenceMatches.length * 0.03);
|
|
750
|
+
let runtimeWeight = 0.05;
|
|
751
|
+
if (runtime?.is_dotnet) {
|
|
752
|
+
runtimeWeight += 0.25;
|
|
753
|
+
}
|
|
754
|
+
if (Array.isArray(runtime?.suspected) && runtime.suspected.length > 0) {
|
|
755
|
+
const topConfidence = Number(runtime.suspected[0]?.confidence || 0);
|
|
756
|
+
runtimeWeight += Math.min(0.45, Math.max(0, topConfidence) * 0.5);
|
|
757
|
+
}
|
|
758
|
+
const total = importWeight + stringWeight + runtimeWeight;
|
|
759
|
+
if (total <= 0) {
|
|
760
|
+
return { import: 0.34, string: 0.33, runtime: 0.33 };
|
|
761
|
+
}
|
|
762
|
+
return {
|
|
763
|
+
import: Number((importWeight / total).toFixed(2)),
|
|
764
|
+
string: Number((stringWeight / total).toFixed(2)),
|
|
765
|
+
runtime: Number((runtimeWeight / total).toFixed(2)),
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
function buildInferenceLayer(threatLevel, yaraMatches, yaraLowConfidenceMatches, suspiciousImports, suspiciousStrings) {
|
|
769
|
+
const hypotheses = [];
|
|
770
|
+
const falsePositiveRisks = [];
|
|
771
|
+
if (yaraMatches.length > 0) {
|
|
772
|
+
hypotheses.push(`YARA medium/high confidence match: ${yaraMatches.slice(0, 5).join(', ')}`);
|
|
773
|
+
}
|
|
774
|
+
if (yaraLowConfidenceMatches.length > 0) {
|
|
775
|
+
hypotheses.push(`YARA low-confidence hints: ${yaraLowConfidenceMatches.slice(0, 5).join(', ')}`);
|
|
776
|
+
falsePositiveRisks.push('Low-confidence YARA hits may be string overlap without strong import/API corroboration.');
|
|
777
|
+
}
|
|
778
|
+
if (suspiciousImports.length > 0) {
|
|
779
|
+
hypotheses.push(`Suspicious API imports observed: ${Math.min(suspiciousImports.length, 10)}`);
|
|
780
|
+
const hasOnlyContextDependentAPIs = suspiciousImports.every((item) => CONTEXT_DEPENDENT_APIS.some((api) => (item.split('!').pop() || item).toLowerCase().includes(api.toLowerCase()))) && suspiciousImports.length > 0;
|
|
781
|
+
if (hasOnlyContextDependentAPIs) {
|
|
782
|
+
falsePositiveRisks.push('Import evidence is mostly context-dependent APIs (debuggers/installers may also use them).');
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
if (suspiciousStrings.length > 0) {
|
|
786
|
+
hypotheses.push(`Behavior-related strings observed: ${Math.min(suspiciousStrings.length, 20)}`);
|
|
787
|
+
}
|
|
788
|
+
let classification = 'unknown';
|
|
789
|
+
if (threatLevel === 'clean') {
|
|
790
|
+
classification = 'benign';
|
|
791
|
+
}
|
|
792
|
+
else if (threatLevel === 'malicious') {
|
|
793
|
+
classification = 'malicious';
|
|
794
|
+
}
|
|
795
|
+
else if (threatLevel === 'suspicious') {
|
|
796
|
+
classification = 'suspicious';
|
|
797
|
+
}
|
|
798
|
+
if (hypotheses.length === 0) {
|
|
799
|
+
hypotheses.push('Insufficient evidence to build high-confidence behavioral inference.');
|
|
800
|
+
}
|
|
801
|
+
return {
|
|
802
|
+
classification,
|
|
803
|
+
hypotheses,
|
|
804
|
+
false_positive_risks: falsePositiveRisks,
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
void [
|
|
808
|
+
calculateThreatLevel,
|
|
809
|
+
generateEvidence,
|
|
810
|
+
generateSummaryAndRecommendation,
|
|
811
|
+
calculateEvidenceWeights,
|
|
812
|
+
buildInferenceLayer,
|
|
813
|
+
];
|
|
814
|
+
function hasMalwareSpecificSignal(signal) {
|
|
815
|
+
return /ransomware|backdoor|downloader|keylogger|stealer|rat|loader/.test(signal.rule.toLowerCase());
|
|
816
|
+
}
|
|
817
|
+
export function calculateThreatLevelV2(yaraSignals, suspiciousImports, suspiciousStrings, intentAssessment) {
|
|
818
|
+
let score = 0;
|
|
819
|
+
const maxScore = 90;
|
|
820
|
+
let yaraScore = 0;
|
|
821
|
+
for (const signal of yaraSignals) {
|
|
822
|
+
const levelWeight = signal.level === 'high'
|
|
823
|
+
? 16
|
|
824
|
+
: signal.level === 'medium'
|
|
825
|
+
? 10
|
|
826
|
+
: signal.level === 'low'
|
|
827
|
+
? 4
|
|
828
|
+
: 6;
|
|
829
|
+
let signalScore = levelWeight + Math.min(4, Math.max(0, signal.score) * 3);
|
|
830
|
+
if (signal.stringOnly) {
|
|
831
|
+
signalScore *= 0.35;
|
|
832
|
+
}
|
|
833
|
+
if (signal.generic) {
|
|
834
|
+
signalScore *= 0.55;
|
|
835
|
+
}
|
|
836
|
+
if ((intentAssessment.label === 'dual_use_tool' ||
|
|
837
|
+
intentAssessment.label === 'operator_utility') &&
|
|
838
|
+
signal.generic) {
|
|
839
|
+
signalScore *= 0.55;
|
|
840
|
+
}
|
|
841
|
+
if (intentAssessment.label === 'dual_use_tool' &&
|
|
842
|
+
signal.stringOnly &&
|
|
843
|
+
!hasMalwareSpecificSignal(signal)) {
|
|
844
|
+
signalScore *= 0.7;
|
|
845
|
+
}
|
|
846
|
+
yaraScore += signalScore;
|
|
847
|
+
}
|
|
848
|
+
score += Math.min(36, yaraScore);
|
|
849
|
+
if (suspiciousImports.length > 0) {
|
|
850
|
+
const importApis = suspiciousImports
|
|
851
|
+
.map((item) => item.split('!').pop() || item)
|
|
852
|
+
.map((item) => item.toLowerCase());
|
|
853
|
+
const highRiskCount = importApis.filter((name) => HIGH_RISK_APIS.some((api) => name.includes(api.toLowerCase()))).length;
|
|
854
|
+
const contextDependentCount = importApis.filter((name) => CONTEXT_DEPENDENT_APIS.some((api) => name.includes(api.toLowerCase()))).length;
|
|
855
|
+
const hasWriteProcessMemory = importApis.some((name) => name.includes('writeprocessmemory'));
|
|
856
|
+
const hasCreateRemoteThread = importApis.some((name) => name.includes('createremotethread'));
|
|
857
|
+
const hasVirtualAllocEx = importApis.some((name) => name.includes('virtualallocex'));
|
|
858
|
+
let importScore = Math.min(highRiskCount * 7, 22) + Math.min(contextDependentCount * 2, 8);
|
|
859
|
+
if (hasWriteProcessMemory && (hasCreateRemoteThread || hasVirtualAllocEx)) {
|
|
860
|
+
importScore += 6;
|
|
861
|
+
}
|
|
862
|
+
if (intentAssessment.label === 'dual_use_tool') {
|
|
863
|
+
importScore *= 0.8;
|
|
864
|
+
}
|
|
865
|
+
else if (intentAssessment.label === 'operator_utility') {
|
|
866
|
+
importScore *= 0.9;
|
|
867
|
+
}
|
|
868
|
+
score += Math.min(28, importScore);
|
|
869
|
+
}
|
|
870
|
+
let stringScore = Math.min(suspiciousStrings.length * 1.4, 18);
|
|
871
|
+
if (intentAssessment.label === 'dual_use_tool') {
|
|
872
|
+
stringScore *= 0.7;
|
|
873
|
+
}
|
|
874
|
+
else if (intentAssessment.label === 'operator_utility') {
|
|
875
|
+
stringScore *= 0.85;
|
|
876
|
+
}
|
|
877
|
+
score += Math.min(18, stringScore);
|
|
878
|
+
if (intentAssessment.label === 'malware_like_payload') {
|
|
879
|
+
score += 8;
|
|
880
|
+
}
|
|
881
|
+
else if (intentAssessment.label === 'dual_use_tool') {
|
|
882
|
+
score -= 4;
|
|
883
|
+
}
|
|
884
|
+
const boundedScore = Math.max(0, score);
|
|
885
|
+
const confidence = Number(Math.max(0, Math.min(1, boundedScore / maxScore)).toFixed(2));
|
|
886
|
+
const strongMalwareYara = yaraSignals.some((signal) => !signal.generic &&
|
|
887
|
+
!signal.stringOnly &&
|
|
888
|
+
hasMalwareSpecificSignal(signal) &&
|
|
889
|
+
signal.level !== 'low');
|
|
890
|
+
let level = 'clean';
|
|
891
|
+
if (strongMalwareYara && boundedScore >= 34 && intentAssessment.label !== 'dual_use_tool') {
|
|
892
|
+
level = 'malicious';
|
|
893
|
+
}
|
|
894
|
+
else if (boundedScore >= 44 && intentAssessment.label !== 'dual_use_tool') {
|
|
895
|
+
level = 'malicious';
|
|
896
|
+
}
|
|
897
|
+
else if (boundedScore >= 12) {
|
|
898
|
+
level = 'suspicious';
|
|
899
|
+
}
|
|
900
|
+
const hasMeaningfulStaticCapability = suspiciousImports.length > 0 || suspiciousStrings.length > 0 || yaraSignals.length > 0;
|
|
901
|
+
if (level === 'clean' &&
|
|
902
|
+
hasMeaningfulStaticCapability &&
|
|
903
|
+
(intentAssessment.label === 'dual_use_tool' || intentAssessment.label === 'operator_utility')) {
|
|
904
|
+
level = 'suspicious';
|
|
905
|
+
}
|
|
906
|
+
return { level, confidence };
|
|
907
|
+
}
|
|
908
|
+
function generateEvidenceV2(yaraSignals, suspiciousImports, suspiciousStrings, runtime, intentAssessment, toolingAssessment) {
|
|
909
|
+
const evidence = [];
|
|
910
|
+
const strongSignals = yaraSignals
|
|
911
|
+
.filter((signal) => signal.level !== 'low')
|
|
912
|
+
.map((signal) => `${signal.rule}${signal.stringOnly ? ' [string-only]' : ''}${signal.generic ? ' [generic]' : ''}`);
|
|
913
|
+
const downgradedSignals = yaraSignals
|
|
914
|
+
.filter((signal) => signal.level === 'low')
|
|
915
|
+
.map((signal) => signal.rule);
|
|
916
|
+
if (strongSignals.length > 0) {
|
|
917
|
+
evidence.push(`YARA medium/high confidence matches: ${strongSignals.slice(0, 6).join(', ')}`);
|
|
918
|
+
}
|
|
919
|
+
if (downgradedSignals.length > 0) {
|
|
920
|
+
evidence.push(`YARA low-confidence hints: ${downgradedSignals.slice(0, 6).join(', ')}`);
|
|
921
|
+
}
|
|
922
|
+
if (suspiciousImports.length > 0) {
|
|
923
|
+
evidence.push(`Suspicious imports observed: ${suspiciousImports.length}`);
|
|
924
|
+
if (suspiciousImports.length <= 5) {
|
|
925
|
+
evidence.push(`Import details: ${suspiciousImports.join(', ')}`);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
if (suspiciousStrings.length > 0) {
|
|
929
|
+
evidence.push(`Behavior-related strings observed: ${suspiciousStrings.length}`);
|
|
930
|
+
}
|
|
931
|
+
if (toolingAssessment.help_text_detected || toolingAssessment.cli_surface_detected) {
|
|
932
|
+
evidence.push('Grouped strings indicate operator-facing help text or CLI options.');
|
|
933
|
+
}
|
|
934
|
+
if (toolingAssessment.library_profile) {
|
|
935
|
+
const librarySummary = summarizeLibraryProfile(toolingAssessment.library_profile);
|
|
936
|
+
if (librarySummary) {
|
|
937
|
+
evidence.push(`Library/crate profile: ${librarySummary}`);
|
|
938
|
+
}
|
|
939
|
+
evidence.push(...toolingAssessment.library_profile.evidence.slice(0, 2));
|
|
940
|
+
}
|
|
941
|
+
if (intentAssessment.evidence.length > 0) {
|
|
942
|
+
evidence.push(...intentAssessment.evidence.slice(0, 2));
|
|
943
|
+
}
|
|
944
|
+
if (runtime?.is_dotnet) {
|
|
945
|
+
evidence.push(`.NET program detected (${runtime.dotnet_version || 'unknown version'})`);
|
|
946
|
+
}
|
|
947
|
+
if (Array.isArray(runtime?.suspected) && runtime.suspected.length > 0) {
|
|
948
|
+
const runtimes = runtime.suspected
|
|
949
|
+
.map((item) => String(item?.runtime || '').trim())
|
|
950
|
+
.filter((item) => item.length > 0);
|
|
951
|
+
if (runtimes.length > 0) {
|
|
952
|
+
evidence.push(`Runtime hints: ${Array.from(new Set(runtimes)).join(', ')}`);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
return Array.from(new Set(evidence));
|
|
956
|
+
}
|
|
957
|
+
function generateSummaryAndRecommendationV2(threatLevel, yaraSignals, runtime, intentAssessment, toolingAssessment) {
|
|
958
|
+
const strongRules = yaraSignals
|
|
959
|
+
.filter((signal) => signal.level !== 'low')
|
|
960
|
+
.map((signal) => signal.rule);
|
|
961
|
+
const packerMatches = strongRules.filter((rule) => /upx|packer|themida|vmprotect/i.test(rule));
|
|
962
|
+
const runtimeSuffix = runtime?.is_dotnet ? ' (.NET)' : '';
|
|
963
|
+
const librarySuffix = summarizeLibraryProfile(toolingAssessment.library_profile)
|
|
964
|
+
? ` Tooling stack hints: ${summarizeLibraryProfile(toolingAssessment.library_profile)}.`
|
|
965
|
+
: '';
|
|
966
|
+
if (intentAssessment.label === 'dual_use_tool') {
|
|
967
|
+
return {
|
|
968
|
+
summary: 'Static evidence is more consistent with a dual-use operator utility than a pure malware payload.' +
|
|
969
|
+
runtimeSuffix +
|
|
970
|
+
librarySuffix,
|
|
971
|
+
recommendation: 'Validate provenance, operator workflow, and deployment context before labeling it malicious. ' +
|
|
972
|
+
'Treat generic or string-only YARA hits as weak until dynamic or function-level evidence confirms abuse.',
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
if (intentAssessment.label === 'operator_utility') {
|
|
976
|
+
return {
|
|
977
|
+
summary: 'The sample exposes an operator-facing CLI/help surface and should be treated as a suspicious utility pending validation.' +
|
|
978
|
+
runtimeSuffix +
|
|
979
|
+
librarySuffix,
|
|
980
|
+
recommendation: 'Correlate with execution context, parent process, and any dropped artifacts before concluding malicious intent.',
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
if (threatLevel === 'malicious') {
|
|
984
|
+
const malwareRules = strongRules.filter((rule) => /trojan|ransomware|backdoor|loader|stealer/i.test(rule));
|
|
985
|
+
return {
|
|
986
|
+
summary: malwareRules.length > 0
|
|
987
|
+
? `Static evidence aligns with malware-like behavior: ${malwareRules.slice(0, 4).join(', ')}${runtimeSuffix}${librarySuffix}`
|
|
988
|
+
: `Static evidence indicates a high-risk malicious payload${runtimeSuffix}${librarySuffix}`,
|
|
989
|
+
recommendation: 'Handle the sample in an isolated environment and collect dynamic evidence before any operational use.',
|
|
990
|
+
};
|
|
991
|
+
}
|
|
992
|
+
if (threatLevel === 'suspicious') {
|
|
993
|
+
if (packerMatches.length > 0) {
|
|
994
|
+
return {
|
|
995
|
+
summary: `Packed or protected traits detected: ${packerMatches.slice(0, 4).join(', ')}${runtimeSuffix}${librarySuffix}`,
|
|
996
|
+
recommendation: 'Unpack or deepen static analysis before making a final malware classification.',
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
return {
|
|
1000
|
+
summary: toolingAssessment.help_text_detected || toolingAssessment.cli_surface_detected
|
|
1001
|
+
? `Suspicious capability set detected, but the sample also exposes an operator-facing surface${runtimeSuffix}${librarySuffix}`
|
|
1002
|
+
: `Suspicious static behavior detected${runtimeSuffix}${librarySuffix}`,
|
|
1003
|
+
recommendation: 'Escalate to deeper static analysis or controlled dynamic execution to resolve intent and capability.',
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
if (threatLevel === 'clean') {
|
|
1007
|
+
return {
|
|
1008
|
+
summary: `No strong malicious indicators were confirmed from current static evidence${runtimeSuffix}${librarySuffix}`,
|
|
1009
|
+
recommendation: 'Retain the sample for context-aware review if provenance is unknown, but current evidence alone is weak.',
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1012
|
+
return {
|
|
1013
|
+
summary: `Threat level could not be determined with current evidence${runtimeSuffix}${librarySuffix}`,
|
|
1014
|
+
recommendation: 'Collect additional static or dynamic evidence before drawing behavioral conclusions.',
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
function calculateEvidenceWeightsV2(suspiciousImports, suspiciousStrings, runtime, yaraSignals, intentAssessment) {
|
|
1018
|
+
let importWeight = Math.min(0.9, suspiciousImports.length * 0.06);
|
|
1019
|
+
const strongYara = yaraSignals.filter((signal) => signal.level !== 'low').length;
|
|
1020
|
+
const weakYara = yaraSignals.length - strongYara;
|
|
1021
|
+
let stringWeight = Math.min(0.8, suspiciousStrings.length * 0.03) +
|
|
1022
|
+
Math.min(0.28, strongYara * 0.07 + weakYara * 0.02);
|
|
1023
|
+
let runtimeWeight = 0.05;
|
|
1024
|
+
if (runtime?.is_dotnet) {
|
|
1025
|
+
runtimeWeight += 0.25;
|
|
1026
|
+
}
|
|
1027
|
+
if (Array.isArray(runtime?.suspected) && runtime.suspected.length > 0) {
|
|
1028
|
+
const topConfidence = Number(runtime.suspected[0]?.confidence || 0);
|
|
1029
|
+
runtimeWeight += Math.min(0.45, Math.max(0, topConfidence) * 0.5);
|
|
1030
|
+
}
|
|
1031
|
+
if (intentAssessment.label === 'dual_use_tool') {
|
|
1032
|
+
stringWeight *= 0.85;
|
|
1033
|
+
}
|
|
1034
|
+
const total = importWeight + stringWeight + runtimeWeight;
|
|
1035
|
+
if (total <= 0) {
|
|
1036
|
+
return { import: 0.34, string: 0.33, runtime: 0.33 };
|
|
1037
|
+
}
|
|
1038
|
+
return {
|
|
1039
|
+
import: Number((importWeight / total).toFixed(2)),
|
|
1040
|
+
string: Number((stringWeight / total).toFixed(2)),
|
|
1041
|
+
runtime: Number((runtimeWeight / total).toFixed(2)),
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
function buildInferenceLayerV2(threatLevel, yaraSignals, suspiciousImports, suspiciousStrings, intentAssessment, toolingAssessment) {
|
|
1045
|
+
const hypotheses = [];
|
|
1046
|
+
const falsePositiveRisks = [];
|
|
1047
|
+
const strongYara = yaraSignals.filter((signal) => signal.level !== 'low');
|
|
1048
|
+
const weakYara = yaraSignals.filter((signal) => signal.level === 'low');
|
|
1049
|
+
if (strongYara.length > 0) {
|
|
1050
|
+
hypotheses.push(`YARA medium/high confidence match: ${strongYara
|
|
1051
|
+
.map((signal) => signal.rule)
|
|
1052
|
+
.slice(0, 5)
|
|
1053
|
+
.join(', ')}`);
|
|
1054
|
+
}
|
|
1055
|
+
if (weakYara.length > 0) {
|
|
1056
|
+
hypotheses.push(`YARA low-confidence hints: ${weakYara
|
|
1057
|
+
.map((signal) => signal.rule)
|
|
1058
|
+
.slice(0, 5)
|
|
1059
|
+
.join(', ')}`);
|
|
1060
|
+
falsePositiveRisks.push('Low-confidence YARA hits may be string overlap without strong import/API corroboration.');
|
|
1061
|
+
}
|
|
1062
|
+
if (strongYara.some((signal) => signal.stringOnly)) {
|
|
1063
|
+
falsePositiveRisks.push('Some medium/high YARA hits remain string-heavy and should not be treated as execution proof.');
|
|
1064
|
+
}
|
|
1065
|
+
if (strongYara.some((signal) => signal.generic)) {
|
|
1066
|
+
falsePositiveRisks.push('Generic malware-family YARA matches can overlap with dual-use process tooling.');
|
|
1067
|
+
}
|
|
1068
|
+
if (suspiciousImports.length > 0) {
|
|
1069
|
+
hypotheses.push(`Suspicious API imports observed: ${Math.min(suspiciousImports.length, 10)}`);
|
|
1070
|
+
}
|
|
1071
|
+
if (suspiciousStrings.length > 0) {
|
|
1072
|
+
hypotheses.push(`Behavior-related strings observed: ${Math.min(suspiciousStrings.length, 20)}`);
|
|
1073
|
+
}
|
|
1074
|
+
if (intentAssessment.evidence.length > 0) {
|
|
1075
|
+
hypotheses.push(...intentAssessment.evidence.slice(0, 2));
|
|
1076
|
+
}
|
|
1077
|
+
if (toolingAssessment.library_profile) {
|
|
1078
|
+
const librarySummary = summarizeLibraryProfile(toolingAssessment.library_profile);
|
|
1079
|
+
if (librarySummary) {
|
|
1080
|
+
hypotheses.push(`Observed crate/library profile: ${librarySummary}`);
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
falsePositiveRisks.push(...intentAssessment.counter_evidence);
|
|
1084
|
+
let classification = 'unknown';
|
|
1085
|
+
if (threatLevel === 'clean') {
|
|
1086
|
+
classification = 'benign';
|
|
1087
|
+
}
|
|
1088
|
+
else if (threatLevel === 'malicious') {
|
|
1089
|
+
classification = 'malicious';
|
|
1090
|
+
}
|
|
1091
|
+
else if (threatLevel === 'suspicious') {
|
|
1092
|
+
classification = 'suspicious';
|
|
1093
|
+
}
|
|
1094
|
+
if (hypotheses.length === 0) {
|
|
1095
|
+
hypotheses.push('Insufficient evidence to build high-confidence behavioral inference.');
|
|
1096
|
+
}
|
|
1097
|
+
return {
|
|
1098
|
+
classification,
|
|
1099
|
+
hypotheses: Array.from(new Set(hypotheses)),
|
|
1100
|
+
false_positive_risks: Array.from(new Set(falsePositiveRisks)),
|
|
1101
|
+
intent_assessment: intentAssessment,
|
|
1102
|
+
tooling_assessment: toolingAssessment,
|
|
1103
|
+
};
|
|
1104
|
+
}
|
|
1105
|
+
// ============================================================================
|
|
1106
|
+
// Standalone Workflow Function
|
|
1107
|
+
// ============================================================================
|
|
1108
|
+
/**
|
|
1109
|
+
* Execute triage workflow
|
|
1110
|
+
* Requirements: 15.1, 15.2, 15.4, 15.5
|
|
1111
|
+
*
|
|
1112
|
+
* This is a standalone function that can be called by other workflows
|
|
1113
|
+
*/
|
|
1114
|
+
export async function triageWorkflow(sampleId, workspaceManager, database, cacheManager) {
|
|
1115
|
+
const handler = createTriageWorkflowHandler(workspaceManager, database, cacheManager);
|
|
1116
|
+
const result = await handler({ sample_id: sampleId });
|
|
1117
|
+
// Convert WorkerResult to TriageWorkflowOutput
|
|
1118
|
+
return {
|
|
1119
|
+
ok: result.ok,
|
|
1120
|
+
data: result.data,
|
|
1121
|
+
errors: result.errors,
|
|
1122
|
+
warnings: result.warnings
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
// ============================================================================
|
|
1126
|
+
// Workflow Handler
|
|
1127
|
+
// ============================================================================
|
|
1128
|
+
/**
|
|
1129
|
+
* Create triage workflow handler
|
|
1130
|
+
* Requirements: 15.1, 15.2, 15.4, 15.5
|
|
1131
|
+
*/
|
|
1132
|
+
export function createTriageWorkflowHandler(workspaceManager, database, cacheManager) {
|
|
1133
|
+
// Create tool handlers
|
|
1134
|
+
const peFingerprintHandler = createPEFingerprintHandler(workspaceManager, database, cacheManager);
|
|
1135
|
+
const runtimeDetectHandler = createRuntimeDetectHandler(workspaceManager, database, cacheManager);
|
|
1136
|
+
const peImportsExtractHandler = createPEImportsExtractHandler(workspaceManager, database, cacheManager);
|
|
1137
|
+
const stringsExtractHandler = createStringsExtractHandler(workspaceManager, database, cacheManager);
|
|
1138
|
+
const yaraScanHandler = createYaraScanHandler(workspaceManager, database, cacheManager);
|
|
1139
|
+
return async (args) => {
|
|
1140
|
+
const input = args;
|
|
1141
|
+
const startTime = Date.now();
|
|
1142
|
+
const warnings = [];
|
|
1143
|
+
const errors = [];
|
|
1144
|
+
try {
|
|
1145
|
+
// Verify sample exists
|
|
1146
|
+
const sample = database.findSample(input.sample_id);
|
|
1147
|
+
if (!sample) {
|
|
1148
|
+
return {
|
|
1149
|
+
ok: false,
|
|
1150
|
+
errors: [`Sample not found: ${input.sample_id}`],
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
1153
|
+
// Step 1: PE Fingerprint (fast mode)
|
|
1154
|
+
// Requirement: 15.1
|
|
1155
|
+
const fingerprintResult = await peFingerprintHandler({
|
|
1156
|
+
sample_id: input.sample_id,
|
|
1157
|
+
fast: true,
|
|
1158
|
+
force_refresh: input.force_refresh,
|
|
1159
|
+
});
|
|
1160
|
+
if (!fingerprintResult.ok) {
|
|
1161
|
+
errors.push('PE fingerprint extraction failed');
|
|
1162
|
+
}
|
|
1163
|
+
if (fingerprintResult.warnings) {
|
|
1164
|
+
warnings.push(...fingerprintResult.warnings);
|
|
1165
|
+
}
|
|
1166
|
+
// Step 2: Runtime Detection
|
|
1167
|
+
// Requirement: 15.1
|
|
1168
|
+
const runtimeResult = await runtimeDetectHandler({
|
|
1169
|
+
sample_id: input.sample_id,
|
|
1170
|
+
force_refresh: input.force_refresh,
|
|
1171
|
+
});
|
|
1172
|
+
if (!runtimeResult.ok) {
|
|
1173
|
+
errors.push('Runtime detection failed');
|
|
1174
|
+
}
|
|
1175
|
+
if (runtimeResult.warnings) {
|
|
1176
|
+
warnings.push(...runtimeResult.warnings);
|
|
1177
|
+
}
|
|
1178
|
+
// Step 3: Import Table Extraction
|
|
1179
|
+
// Requirement: 15.1
|
|
1180
|
+
const importsResult = await peImportsExtractHandler({
|
|
1181
|
+
sample_id: input.sample_id,
|
|
1182
|
+
group_by_dll: true,
|
|
1183
|
+
force_refresh: input.force_refresh,
|
|
1184
|
+
});
|
|
1185
|
+
if (!importsResult.ok) {
|
|
1186
|
+
errors.push('Import table extraction failed');
|
|
1187
|
+
}
|
|
1188
|
+
if (importsResult.warnings) {
|
|
1189
|
+
warnings.push(...importsResult.warnings);
|
|
1190
|
+
}
|
|
1191
|
+
// Step 4: String Extraction
|
|
1192
|
+
// Requirement: 15.1
|
|
1193
|
+
const stringsResult = await stringsExtractHandler({
|
|
1194
|
+
sample_id: input.sample_id,
|
|
1195
|
+
min_len: 6,
|
|
1196
|
+
encoding: 'all',
|
|
1197
|
+
force_refresh: input.force_refresh,
|
|
1198
|
+
});
|
|
1199
|
+
if (!stringsResult.ok) {
|
|
1200
|
+
errors.push('String extraction failed');
|
|
1201
|
+
}
|
|
1202
|
+
if (stringsResult.warnings) {
|
|
1203
|
+
warnings.push(...stringsResult.warnings);
|
|
1204
|
+
}
|
|
1205
|
+
// Step 5: YARA Scan
|
|
1206
|
+
// Requirement: 15.1
|
|
1207
|
+
const yaraResult = await yaraScanHandler({
|
|
1208
|
+
sample_id: input.sample_id,
|
|
1209
|
+
rule_set: 'malware_families',
|
|
1210
|
+
rule_tier: 'production',
|
|
1211
|
+
force_refresh: input.force_refresh,
|
|
1212
|
+
});
|
|
1213
|
+
if (!yaraResult.ok) {
|
|
1214
|
+
errors.push('YARA scan failed');
|
|
1215
|
+
}
|
|
1216
|
+
if (yaraResult.warnings) {
|
|
1217
|
+
warnings.push(...yaraResult.warnings);
|
|
1218
|
+
}
|
|
1219
|
+
// If all tools failed, return error
|
|
1220
|
+
if (errors.length >= 5) {
|
|
1221
|
+
return {
|
|
1222
|
+
ok: false,
|
|
1223
|
+
errors: ['All analysis tools failed', ...errors],
|
|
1224
|
+
warnings,
|
|
1225
|
+
metrics: {
|
|
1226
|
+
elapsed_ms: Date.now() - startTime,
|
|
1227
|
+
tool: TOOL_NAME,
|
|
1228
|
+
},
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1231
|
+
// Step 6: Aggregate results and generate structured summary
|
|
1232
|
+
// Requirements: 15.2, 15.4, 15.5
|
|
1233
|
+
// Extract YARA matches
|
|
1234
|
+
let yaraSignals = [];
|
|
1235
|
+
if (yaraResult.ok && yaraResult.data) {
|
|
1236
|
+
const yaraData = yaraResult.data;
|
|
1237
|
+
if (yaraData.matches && Array.isArray(yaraData.matches)) {
|
|
1238
|
+
yaraSignals = normalizeYaraSignals(yaraData.matches);
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
// Analyze imports for suspicious APIs
|
|
1242
|
+
const suspiciousImports = [];
|
|
1243
|
+
if (importsResult.ok && importsResult.data) {
|
|
1244
|
+
const importsData = importsResult.data;
|
|
1245
|
+
if (importsData.imports && typeof importsData.imports === 'object') {
|
|
1246
|
+
suspiciousImports.push(...analyzeSuspiciousImports(importsData.imports));
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
// Analyze strings for suspicious patterns
|
|
1250
|
+
const stringAnalysis = {
|
|
1251
|
+
suspicious: [],
|
|
1252
|
+
urls: [],
|
|
1253
|
+
ips: [],
|
|
1254
|
+
paths: [],
|
|
1255
|
+
registry: [],
|
|
1256
|
+
commands: [],
|
|
1257
|
+
pipes: [],
|
|
1258
|
+
cargoPaths: [],
|
|
1259
|
+
rustMarkers: [],
|
|
1260
|
+
crateNames: [],
|
|
1261
|
+
libraryHints: [],
|
|
1262
|
+
};
|
|
1263
|
+
if (stringsResult.ok && stringsResult.data) {
|
|
1264
|
+
const stringsData = stringsResult.data;
|
|
1265
|
+
if (stringsData.strings && Array.isArray(stringsData.strings)) {
|
|
1266
|
+
const analysis = analyzeSuspiciousStrings(stringsData.strings);
|
|
1267
|
+
stringAnalysis.suspicious = analysis.suspicious;
|
|
1268
|
+
stringAnalysis.urls = analysis.urls;
|
|
1269
|
+
stringAnalysis.ips = analysis.ips;
|
|
1270
|
+
stringAnalysis.paths = analysis.paths;
|
|
1271
|
+
stringAnalysis.registry = analysis.registry;
|
|
1272
|
+
stringAnalysis.commands = analysis.commands;
|
|
1273
|
+
stringAnalysis.pipes = analysis.pipes;
|
|
1274
|
+
stringAnalysis.cargoPaths = analysis.cargoPaths;
|
|
1275
|
+
stringAnalysis.rustMarkers = analysis.rustMarkers;
|
|
1276
|
+
stringAnalysis.crateNames = analysis.crateNames;
|
|
1277
|
+
stringAnalysis.libraryHints = analysis.libraryHints;
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
const stringsSummary = stringsResult.data?.summary;
|
|
1281
|
+
const { intent, tooling } = assessIntentAndTooling(stringsSummary, suspiciousImports, stringAnalysis, yaraSignals, runtimeResult.data);
|
|
1282
|
+
yaraSignals = applyIntentAwareYaraAdjustments(yaraSignals, intent);
|
|
1283
|
+
const yaraMatches = Array.from(new Set(yaraSignals
|
|
1284
|
+
.filter((signal) => signal.level !== 'low')
|
|
1285
|
+
.map((signal) => signal.rule)));
|
|
1286
|
+
const yaraLowConfidenceMatches = Array.from(new Set(yaraSignals
|
|
1287
|
+
.filter((signal) => signal.level === 'low')
|
|
1288
|
+
.map((signal) => signal.rule)));
|
|
1289
|
+
// Calculate threat level and confidence
|
|
1290
|
+
const { level: threatLevel, confidence } = calculateThreatLevelV2(yaraSignals, suspiciousImports, stringAnalysis.suspicious, intent);
|
|
1291
|
+
// Generate evidence
|
|
1292
|
+
const evidence = generateEvidenceV2(yaraSignals, suspiciousImports, stringAnalysis.suspicious, runtimeResult.data, intent, tooling);
|
|
1293
|
+
if (yaraLowConfidenceMatches.length > 0) {
|
|
1294
|
+
evidence.push(`YARA low-confidence matches (downgraded): ${yaraLowConfidenceMatches.join(', ')}`);
|
|
1295
|
+
}
|
|
1296
|
+
// Generate summary and recommendation
|
|
1297
|
+
const { summary, recommendation } = generateSummaryAndRecommendationV2(threatLevel, yaraSignals, runtimeResult.data, intent, tooling);
|
|
1298
|
+
const inference = buildInferenceLayerV2(threatLevel, yaraSignals, suspiciousImports, stringAnalysis.suspicious, intent, tooling);
|
|
1299
|
+
const evidenceWeights = calculateEvidenceWeightsV2(suspiciousImports, stringAnalysis.suspicious, runtimeResult.data, yaraSignals, intent);
|
|
1300
|
+
const highValueIocs = {
|
|
1301
|
+
suspicious_apis: suspiciousImports.length > 0 ? suspiciousImports.slice(0, 20) : undefined,
|
|
1302
|
+
commands: stringAnalysis.commands.length > 0 ? stringAnalysis.commands.slice(0, 15) : undefined,
|
|
1303
|
+
pipes: stringAnalysis.pipes.length > 0 ? stringAnalysis.pipes.slice(0, 15) : undefined,
|
|
1304
|
+
urls: stringAnalysis.urls.length > 0 ? stringAnalysis.urls.slice(0, 15) : undefined,
|
|
1305
|
+
network: stringAnalysis.ips.length > 0 ? stringAnalysis.ips.slice(0, 15) : undefined,
|
|
1306
|
+
};
|
|
1307
|
+
const compilerArtifacts = {
|
|
1308
|
+
cargo_paths: stringAnalysis.cargoPaths.length > 0 ? stringAnalysis.cargoPaths.slice(0, 10) : undefined,
|
|
1309
|
+
rust_markers: stringAnalysis.rustMarkers.length > 0 ? stringAnalysis.rustMarkers.slice(0, 10) : undefined,
|
|
1310
|
+
library_profile: tooling.library_profile,
|
|
1311
|
+
};
|
|
1312
|
+
const hasHighValue = Boolean(highValueIocs.suspicious_apis?.length) ||
|
|
1313
|
+
Boolean(highValueIocs.commands?.length) ||
|
|
1314
|
+
Boolean(highValueIocs.pipes?.length) ||
|
|
1315
|
+
Boolean(highValueIocs.urls?.length) ||
|
|
1316
|
+
Boolean(highValueIocs.network?.length);
|
|
1317
|
+
const hasCompilerArtifacts = Boolean(compilerArtifacts.cargo_paths?.length) ||
|
|
1318
|
+
Boolean(compilerArtifacts.rust_markers?.length) ||
|
|
1319
|
+
Boolean(compilerArtifacts.library_profile);
|
|
1320
|
+
// Build IOCs
|
|
1321
|
+
const iocs = {
|
|
1322
|
+
suspicious_imports: suspiciousImports,
|
|
1323
|
+
suspicious_strings: stringAnalysis.suspicious.slice(0, 20), // Limit to top 20
|
|
1324
|
+
yara_matches: yaraMatches,
|
|
1325
|
+
yara_low_confidence: yaraLowConfidenceMatches.length > 0 ? yaraLowConfidenceMatches : undefined,
|
|
1326
|
+
urls: stringAnalysis.urls.length > 0 ? stringAnalysis.urls : undefined,
|
|
1327
|
+
ip_addresses: stringAnalysis.ips.length > 0 ? stringAnalysis.ips : undefined,
|
|
1328
|
+
file_paths: stringAnalysis.paths.length > 0 ? stringAnalysis.paths.slice(0, 10) : undefined,
|
|
1329
|
+
registry_keys: stringAnalysis.registry.length > 0 ? stringAnalysis.registry.slice(0, 10) : undefined,
|
|
1330
|
+
high_value_iocs: hasHighValue ? highValueIocs : undefined,
|
|
1331
|
+
compiler_artifacts: hasCompilerArtifacts ? compilerArtifacts : undefined,
|
|
1332
|
+
};
|
|
1333
|
+
// Return structured result
|
|
1334
|
+
return {
|
|
1335
|
+
ok: true,
|
|
1336
|
+
data: {
|
|
1337
|
+
summary,
|
|
1338
|
+
confidence,
|
|
1339
|
+
threat_level: threatLevel,
|
|
1340
|
+
iocs,
|
|
1341
|
+
evidence,
|
|
1342
|
+
evidence_weights: evidenceWeights,
|
|
1343
|
+
inference,
|
|
1344
|
+
recommendation,
|
|
1345
|
+
raw_results: {
|
|
1346
|
+
fingerprint: fingerprintResult.data || null,
|
|
1347
|
+
runtime: runtimeResult.data || null,
|
|
1348
|
+
imports: importsResult.data || null,
|
|
1349
|
+
strings: stringsResult.data || null,
|
|
1350
|
+
yara: yaraResult.data || null,
|
|
1351
|
+
},
|
|
1352
|
+
},
|
|
1353
|
+
warnings: warnings.length > 0 ? warnings : undefined,
|
|
1354
|
+
errors: errors.length > 0 ? errors : undefined,
|
|
1355
|
+
metrics: {
|
|
1356
|
+
elapsed_ms: Date.now() - startTime,
|
|
1357
|
+
tool: TOOL_NAME,
|
|
1358
|
+
},
|
|
1359
|
+
};
|
|
1360
|
+
}
|
|
1361
|
+
catch (error) {
|
|
1362
|
+
return {
|
|
1363
|
+
ok: false,
|
|
1364
|
+
errors: [error.message, ...errors],
|
|
1365
|
+
warnings: warnings.length > 0 ? warnings : undefined,
|
|
1366
|
+
metrics: {
|
|
1367
|
+
elapsed_ms: Date.now() - startTime,
|
|
1368
|
+
tool: TOOL_NAME,
|
|
1369
|
+
},
|
|
1370
|
+
};
|
|
1371
|
+
}
|
|
1372
|
+
};
|
|
1373
|
+
}
|
|
1374
|
+
//# sourceMappingURL=triage.js.map
|