malshare-sdk 1.0.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/CHANGELOG.md +15 -0
- package/LICENSE +21 -0
- package/README.md +366 -0
- package/dist/index.js +215 -0
- package/package.json +59 -0
- package/src/cli.js +210 -0
- package/src/index.d.ts +309 -0
- package/src/index.js +404 -0
package/src/index.js
ADDED
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MalShare SDK — JavaScript client for the MalShare API.
|
|
3
|
+
*
|
|
4
|
+
* MalShare is a free malware sample repository with 1M+ samples.
|
|
5
|
+
* API: https://malshare.com/doc.php
|
|
6
|
+
* Registration: https://malshare.com/register.php
|
|
7
|
+
*
|
|
8
|
+
* Works on: Bun, Node.js, Deno, Cloudflare Workers.
|
|
9
|
+
* Zero dependencies. Uses native fetch() or Bun/node:http.
|
|
10
|
+
*
|
|
11
|
+
* @module malshare-sdk
|
|
12
|
+
* @license MIT
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* import { MalShare } from 'malshare-sdk';
|
|
16
|
+
* const ms = new MalShare('your-api-key');
|
|
17
|
+
* const hashes = await ms.listSamples();
|
|
18
|
+
* const sample = await ms.download('abc123...');
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
// ══════════════════════════════════════════════════════════════════
|
|
22
|
+
// TYPES
|
|
23
|
+
// ══════════════════════════════════════════════════════════════════
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @typedef {Object} MalShareConfig
|
|
27
|
+
* @property {string} apiKey — MalShare API key
|
|
28
|
+
* @property {string} [baseUrl='https://malshare.com/api.php'] — API base URL
|
|
29
|
+
* @property {number} [timeoutMs=60000] — request timeout in ms
|
|
30
|
+
* @property {Function} [fetch] — custom fetch implementation (for CF Workers etc.)
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @typedef {Object} FileDetails
|
|
35
|
+
* @property {string} MD5 — MD5 hash
|
|
36
|
+
* @property {string} SHA1 — SHA1 hash
|
|
37
|
+
* @property {string} SHA256 — SHA256 hash
|
|
38
|
+
* @property {string} F_TYPE — file type (e.g. 'PE32 executable')
|
|
39
|
+
* @property {number} F_SIZE — file size in bytes
|
|
40
|
+
* @property {string} [F_NAME] — original file name
|
|
41
|
+
* @property {string[]} [SOURCES] — sample sources
|
|
42
|
+
* @property {string} [FIRST_SEEN] — first seen timestamp
|
|
43
|
+
* @property {string} [LAST_SEEN] — last seen timestamp
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @typedef {Object} UploadResult
|
|
48
|
+
* @property {string} status — 'OK' or 'ERROR'
|
|
49
|
+
* @property {string} [guid] — upload GUID for status tracking
|
|
50
|
+
* @property {string} [error] — error message if status is ERROR
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @typedef {Object} DownloadUrlResult
|
|
55
|
+
* @property {string} status — 'OK' or 'ERROR'
|
|
56
|
+
* @property {string} [guid] — task GUID for checking status
|
|
57
|
+
* @property {string} [error] — error message
|
|
58
|
+
*/
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @typedef {Object} DownloadUrlStatus
|
|
62
|
+
* @property {string} status — 'missing' | 'pending' | 'processing' | 'finished'
|
|
63
|
+
* @property {string} [guid] — task GUID
|
|
64
|
+
*/
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @typedef {Object} QuotaInfo
|
|
68
|
+
* @property {number} limit — allocated API calls per day
|
|
69
|
+
* @property {number} remaining — remaining API calls
|
|
70
|
+
*/
|
|
71
|
+
|
|
72
|
+
// ══════════════════════════════════════════════════════════════════
|
|
73
|
+
// MAIN CLASS
|
|
74
|
+
// ══════════════════════════════════════════════════════════════════
|
|
75
|
+
|
|
76
|
+
export class MalShare {
|
|
77
|
+
/**
|
|
78
|
+
* @param {string} apiKey — MalShare API key (free at https://malshare.com/register.php)
|
|
79
|
+
* @param {Partial<MalShareConfig>} [config]
|
|
80
|
+
*/
|
|
81
|
+
constructor(apiKey, config = {}) {
|
|
82
|
+
if (!apiKey) throw new Error('MalShare requires an API key. Register at https://malshare.com/register.php');
|
|
83
|
+
|
|
84
|
+
/** @type {MalShareConfig} */
|
|
85
|
+
this.config = {
|
|
86
|
+
apiKey,
|
|
87
|
+
baseUrl: config.baseUrl || 'https://malshare.com/api.php',
|
|
88
|
+
timeoutMs: config.timeoutMs || 60000,
|
|
89
|
+
fetch: config.fetch || globalThis.fetch,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ═══════════════ INTERNAL ═══════════════
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Make an API request.
|
|
97
|
+
* @param {string} action — API action
|
|
98
|
+
* @param {Object} [params={}] — query parameters
|
|
99
|
+
* @param {Object} [opts={}] — request options
|
|
100
|
+
* @returns {Promise<any>}
|
|
101
|
+
*/
|
|
102
|
+
async _request(action, params = {}, opts = {}) {
|
|
103
|
+
const { raw = false, method = 'GET' } = opts;
|
|
104
|
+
const qs = new URLSearchParams({ api_key: this.config.apiKey, action, ...params });
|
|
105
|
+
const url = `${this.config.baseUrl}?${qs}`;
|
|
106
|
+
|
|
107
|
+
const controller = new AbortController();
|
|
108
|
+
const timer = setTimeout(() => controller.abort(), this.config.timeoutMs);
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
const res = await this.config.fetch(url, {
|
|
112
|
+
method,
|
|
113
|
+
signal: controller.signal,
|
|
114
|
+
headers: { 'Accept': 'application/json, text/plain, */*' },
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
if (!res.ok) {
|
|
118
|
+
throw new Error(`MalShare API error: ${res.status} ${res.statusText}`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (raw) {
|
|
122
|
+
const buf = await res.arrayBuffer();
|
|
123
|
+
return new Uint8Array(buf);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const text = await res.text();
|
|
127
|
+
try {
|
|
128
|
+
return JSON.parse(text);
|
|
129
|
+
} catch {
|
|
130
|
+
// Some endpoints return raw text (e.g. search)
|
|
131
|
+
return text;
|
|
132
|
+
}
|
|
133
|
+
} finally {
|
|
134
|
+
clearTimeout(timer);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ═══════════════ SAMPLE LISTING ═══════════════
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* List SHA256 hashes from the past 24 hours.
|
|
142
|
+
* @returns {Promise<string[]>}
|
|
143
|
+
*/
|
|
144
|
+
async listSamples() {
|
|
145
|
+
const data = await this._request('getlist');
|
|
146
|
+
return Array.isArray(data) ? data : [];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* List SHA256 hashes from the past 24 hours (raw text, one per line).
|
|
151
|
+
* @returns {Promise<string>}
|
|
152
|
+
*/
|
|
153
|
+
async listSamplesRaw() {
|
|
154
|
+
return this._request('getlistraw');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* List sample sources from the past 24 hours.
|
|
159
|
+
* @returns {Promise<{source: string, count: number}[]>}
|
|
160
|
+
*/
|
|
161
|
+
async listSources() {
|
|
162
|
+
return this._request('getsources');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* List sample sources from the past 24 hours (raw text).
|
|
167
|
+
* @returns {Promise<string>}
|
|
168
|
+
*/
|
|
169
|
+
async listSourcesRaw() {
|
|
170
|
+
return this._request('getsourcesraw');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* List file names from uploads in the past 24 hours.
|
|
175
|
+
* @returns {Promise<string[]>}
|
|
176
|
+
*/
|
|
177
|
+
async listFileNames() {
|
|
178
|
+
return this._request('getfilenames');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Get file types and counts from the past 24 hours.
|
|
183
|
+
* @returns {Promise<{type: string, count: number}[]>}
|
|
184
|
+
*/
|
|
185
|
+
async listTypes() {
|
|
186
|
+
return this._request('gettypes');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ═══════════════ SAMPLE INFO ═══════════════
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Get detailed information about a sample.
|
|
193
|
+
*
|
|
194
|
+
* @param {string} hash — MD5, SHA1, or SHA256 hash
|
|
195
|
+
* @returns {Promise<FileDetails|null>}
|
|
196
|
+
*
|
|
197
|
+
* @example
|
|
198
|
+
* const details = await ms.details('46faab8ab153fae...');
|
|
199
|
+
* console.log(details.F_TYPE, details.MD5);
|
|
200
|
+
*/
|
|
201
|
+
async details(hash) {
|
|
202
|
+
const data = await this._request('details', { hash });
|
|
203
|
+
if (!data || data.status === 'ERROR' || !data.SHA256) return null;
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
MD5: data.MD5 || '',
|
|
207
|
+
SHA1: data.SHA1 || '',
|
|
208
|
+
SHA256: data.SHA256 || '',
|
|
209
|
+
F_TYPE: data.F_TYPE || '',
|
|
210
|
+
F_SIZE: data.F_SIZE || 0,
|
|
211
|
+
F_NAME: data.F_NAME || '',
|
|
212
|
+
SOURCES: data.SOURCES || [],
|
|
213
|
+
FIRST_SEEN: data.FIRST_SEEN || '',
|
|
214
|
+
LAST_SEEN: data.LAST_SEEN || '',
|
|
215
|
+
...data,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Bulk hash lookup — supply an array of hashes.
|
|
221
|
+
*
|
|
222
|
+
* @param {string[]} hashes — array of hex-encoded hashes
|
|
223
|
+
* @returns {Promise<FileDetails[]>}
|
|
224
|
+
*
|
|
225
|
+
* @example
|
|
226
|
+
* const results = await ms.hashLookup(['abc123...', 'def456...']);
|
|
227
|
+
*/
|
|
228
|
+
async hashLookup(hashes) {
|
|
229
|
+
if (!Array.isArray(hashes) || hashes.length === 0) return [];
|
|
230
|
+
|
|
231
|
+
const formData = new URLSearchParams();
|
|
232
|
+
formData.append('hashes', JSON.stringify(hashes));
|
|
233
|
+
|
|
234
|
+
const qs = new URLSearchParams({ api_key: this.config.apiKey, action: 'hashlookup' });
|
|
235
|
+
const url = `${this.config.baseUrl}?${qs}`;
|
|
236
|
+
|
|
237
|
+
const controller = new AbortController();
|
|
238
|
+
const timer = setTimeout(() => controller.abort(), this.config.timeoutMs);
|
|
239
|
+
|
|
240
|
+
try {
|
|
241
|
+
const res = await this.config.fetch(url, {
|
|
242
|
+
method: 'POST',
|
|
243
|
+
body: formData,
|
|
244
|
+
signal: controller.signal,
|
|
245
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
246
|
+
});
|
|
247
|
+
if (!res.ok) throw new Error(`MalShare API error: ${res.status}`);
|
|
248
|
+
return res.json();
|
|
249
|
+
} finally {
|
|
250
|
+
clearTimeout(timer);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Search by file type (samples from past 24h).
|
|
256
|
+
*
|
|
257
|
+
* @param {string} fileType — e.g. 'PE32', 'ELF', 'PDF', 'Zip', 'JavaScript'
|
|
258
|
+
* @returns {Promise<string[]>} — array of MD5/SHA1/SHA256 hashes
|
|
259
|
+
*
|
|
260
|
+
* @example
|
|
261
|
+
* const hashes = await ms.searchByType('PE32 executable');
|
|
262
|
+
*/
|
|
263
|
+
async searchByType(fileType) {
|
|
264
|
+
const data = await this._request('type', { type: fileType });
|
|
265
|
+
return Array.isArray(data) ? data : [];
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Search samples by query (hash, source, or file name).
|
|
270
|
+
* Returns raw text results.
|
|
271
|
+
*
|
|
272
|
+
* @param {string} query — search term
|
|
273
|
+
* @returns {Promise<string>} — raw search results
|
|
274
|
+
*/
|
|
275
|
+
async search(query) {
|
|
276
|
+
return this._request('search', { query });
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ═══════════════ SAMPLE DOWNLOAD ═══════════════
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Download a malware sample by hash.
|
|
283
|
+
* Returns raw bytes. **Handle with care — this is live malware.**
|
|
284
|
+
*
|
|
285
|
+
* @param {string} hash — MD5, SHA1, or SHA256 hash
|
|
286
|
+
* @returns {Promise<Uint8Array>} — raw file bytes
|
|
287
|
+
*
|
|
288
|
+
* @example
|
|
289
|
+
* const bytes = await ms.download('46faab8ab153fae...');
|
|
290
|
+
* await Bun.write('/tmp/malware.bin', bytes);
|
|
291
|
+
*/
|
|
292
|
+
async download(hash) {
|
|
293
|
+
return this._request('getfile', { hash }, { raw: true });
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Download a sample and save to disk (Bun/Node only).
|
|
298
|
+
*
|
|
299
|
+
* @param {string} hash — file hash
|
|
300
|
+
* @param {string} filePath — where to save
|
|
301
|
+
* @returns {Promise<{path: string, size: number}>}
|
|
302
|
+
*/
|
|
303
|
+
async downloadTo(hash, filePath) {
|
|
304
|
+
const bytes = await this.download(hash);
|
|
305
|
+
if (typeof Bun !== 'undefined') {
|
|
306
|
+
await Bun.write(filePath, bytes);
|
|
307
|
+
} else {
|
|
308
|
+
const { writeFileSync } = await import('node:fs');
|
|
309
|
+
writeFileSync(filePath, bytes);
|
|
310
|
+
}
|
|
311
|
+
return { path: filePath, size: bytes.length };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// ═══════════════ UPLOAD ═══════════════
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Upload a malware sample.
|
|
318
|
+
* Uploading files temporarily increases your API quota.
|
|
319
|
+
*
|
|
320
|
+
* @param {Uint8Array|Buffer|Blob} file — the sample to upload
|
|
321
|
+
* @param {string} [fileName='sample.bin'] — file name
|
|
322
|
+
* @returns {Promise<UploadResult>}
|
|
323
|
+
*/
|
|
324
|
+
async upload(file, fileName = 'sample.bin') {
|
|
325
|
+
const formData = new FormData();
|
|
326
|
+
formData.append('upload', new Blob([file]), fileName);
|
|
327
|
+
|
|
328
|
+
const qs = new URLSearchParams({ api_key: this.config.apiKey, action: 'upload' });
|
|
329
|
+
const url = `${this.config.baseUrl}?${qs}`;
|
|
330
|
+
|
|
331
|
+
const controller = new AbortController();
|
|
332
|
+
const timer = setTimeout(() => controller.abort(), this.config.timeoutMs * 2); // uploads can be slow
|
|
333
|
+
|
|
334
|
+
try {
|
|
335
|
+
const res = await this.config.fetch(url, {
|
|
336
|
+
method: 'POST',
|
|
337
|
+
body: formData,
|
|
338
|
+
signal: controller.signal,
|
|
339
|
+
});
|
|
340
|
+
return res.json();
|
|
341
|
+
} finally {
|
|
342
|
+
clearTimeout(timer);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// ═══════════════ URL DOWNLOAD ═══════════════
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Submit a URL for MalShare to download and add to its collection.
|
|
350
|
+
*
|
|
351
|
+
* @param {string} url — URL to download
|
|
352
|
+
* @param {boolean} [recursive=false] — enable crawling
|
|
353
|
+
* @returns {Promise<DownloadUrlResult>}
|
|
354
|
+
*/
|
|
355
|
+
async downloadUrl(url, recursive = false) {
|
|
356
|
+
const formData = new URLSearchParams();
|
|
357
|
+
formData.append('url', url);
|
|
358
|
+
if (recursive) formData.append('recursive', '1');
|
|
359
|
+
|
|
360
|
+
const qs = new URLSearchParams({ api_key: this.config.apiKey, action: 'download_url' });
|
|
361
|
+
const reqUrl = `${this.config.baseUrl}?${qs}`;
|
|
362
|
+
|
|
363
|
+
const controller = new AbortController();
|
|
364
|
+
const timer = setTimeout(() => controller.abort(), this.config.timeoutMs);
|
|
365
|
+
|
|
366
|
+
try {
|
|
367
|
+
const res = await this.config.fetch(reqUrl, {
|
|
368
|
+
method: 'POST',
|
|
369
|
+
body: formData,
|
|
370
|
+
signal: controller.signal,
|
|
371
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
372
|
+
});
|
|
373
|
+
return res.json();
|
|
374
|
+
} finally {
|
|
375
|
+
clearTimeout(timer);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Check the status of a URL download task.
|
|
381
|
+
*
|
|
382
|
+
* @param {string} guid — task GUID from downloadUrl()
|
|
383
|
+
* @returns {Promise<DownloadUrlStatus>}
|
|
384
|
+
*/
|
|
385
|
+
async downloadUrlStatus(guid) {
|
|
386
|
+
return this._request('download_url_check', { guid });
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// ═══════════════ QUOTA ═══════════════
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Get API quota information.
|
|
393
|
+
* @returns {Promise<QuotaInfo>}
|
|
394
|
+
*/
|
|
395
|
+
async getQuota() {
|
|
396
|
+
const text = await this._request('getlimit');
|
|
397
|
+
// Response format: "LIMIT: 2000\nREMAINING: 1523"
|
|
398
|
+
const limit = parseInt(text.match(/LIMIT:\s*(\d+)/i)?.[1] || '0');
|
|
399
|
+
const remaining = parseInt(text.match(/REMAINING:\s*(\d+)/i)?.[1] || '0');
|
|
400
|
+
return { limit, remaining };
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
export default MalShare;
|