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/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;