@uploadista/flow-security-nodes 0.0.16-beta.1 → 0.0.16-beta.3
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/.turbo/turbo-build.log +2 -2
- package/package.json +3 -3
- package/tests/scan-virus-node.test.ts +423 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
|
|
2
2
|
|
|
3
|
-
> @uploadista/flow-security-nodes@0.0.
|
|
3
|
+
> @uploadista/flow-security-nodes@0.0.16-beta.2 build /Users/denislaboureyras/Documents/uploadista/dev/uploadista-workspace/uploadista-sdk/packages/flow/security/nodes
|
|
4
4
|
> tsdown
|
|
5
5
|
|
|
6
6
|
[34mℹ[39m tsdown [2mv0.16.5[22m powered by rolldown [2mv1.0.0-beta.50[22m
|
|
@@ -19,4 +19,4 @@
|
|
|
19
19
|
[34mℹ[39m [33m[CJS][39m [2mdist/[22mindex.d.cts.map [2m0.54 kB[22m [2m│ gzip: 0.28 kB[22m
|
|
20
20
|
[34mℹ[39m [33m[CJS][39m [2mdist/[22m[32m[1mindex.d.cts[22m[39m [2m3.18 kB[22m [2m│ gzip: 1.16 kB[22m
|
|
21
21
|
[34mℹ[39m [33m[CJS][39m 2 files, total: 3.72 kB
|
|
22
|
-
[32m✔[39m Build complete in [
|
|
22
|
+
[32m✔[39m Build complete in [32m6119ms[39m
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uploadista/flow-security-nodes",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.0.16-beta.
|
|
4
|
+
"version": "0.0.16-beta.3",
|
|
5
5
|
"description": "Security processing nodes for Uploadista Flow",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Uploadista",
|
|
@@ -16,14 +16,14 @@
|
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"effect": "3.19.4",
|
|
18
18
|
"zod": "4.1.12",
|
|
19
|
-
"@uploadista/core": "0.0.16-beta.
|
|
19
|
+
"@uploadista/core": "0.0.16-beta.3"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"@effect/vitest": "0.27.0",
|
|
23
23
|
"@types/node": "24.10.1",
|
|
24
24
|
"tsdown": "0.16.5",
|
|
25
25
|
"vitest": "4.0.8",
|
|
26
|
-
"@uploadista/typescript-config": "0.0.16-beta.
|
|
26
|
+
"@uploadista/typescript-config": "0.0.16-beta.3"
|
|
27
27
|
},
|
|
28
28
|
"scripts": {
|
|
29
29
|
"build": "tsdown",
|
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
import { describe, expect, it } from "@effect/vitest";
|
|
2
|
+
import { UploadistaError } from "@uploadista/core/errors";
|
|
3
|
+
import {
|
|
4
|
+
TestUploadServer,
|
|
5
|
+
TestVirusScanPlugin,
|
|
6
|
+
} from "@uploadista/core/testing";
|
|
7
|
+
import type { UploadFile } from "@uploadista/core/types";
|
|
8
|
+
import { Effect, Layer } from "effect";
|
|
9
|
+
import { createScanVirusNode } from "../src/scan-virus-node";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* EICAR test file signature (standard antivirus test file)
|
|
13
|
+
* This is a safe, non-malicious string used to test antivirus software
|
|
14
|
+
*/
|
|
15
|
+
const EICAR_SIGNATURE =
|
|
16
|
+
"X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Test utilities for creating sample data
|
|
20
|
+
*/
|
|
21
|
+
const createTestUploadFile = (overrides?: Partial<UploadFile>): UploadFile => ({
|
|
22
|
+
id: "test-file-1",
|
|
23
|
+
offset: 0,
|
|
24
|
+
size: 1024,
|
|
25
|
+
storage: {
|
|
26
|
+
id: "test-storage",
|
|
27
|
+
type: "memory",
|
|
28
|
+
},
|
|
29
|
+
metadata: {
|
|
30
|
+
mimeType: "application/octet-stream",
|
|
31
|
+
originalName: "test-file.bin",
|
|
32
|
+
fileName: "test-file.bin",
|
|
33
|
+
extension: "bin",
|
|
34
|
+
},
|
|
35
|
+
url: "https://example.com/test-file.bin",
|
|
36
|
+
creationDate: new Date().toISOString(),
|
|
37
|
+
...overrides,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Create test file bytes (clean file)
|
|
42
|
+
*/
|
|
43
|
+
const createCleanFileBytes = (): Uint8Array => {
|
|
44
|
+
const encoder = new TextEncoder();
|
|
45
|
+
return encoder.encode("This is a clean test file");
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Create test file bytes (infected with EICAR signature)
|
|
50
|
+
*/
|
|
51
|
+
const createInfectedFileBytes = (): Uint8Array => {
|
|
52
|
+
const encoder = new TextEncoder();
|
|
53
|
+
return encoder.encode(EICAR_SIGNATURE);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Test layer combining all mocks
|
|
58
|
+
*/
|
|
59
|
+
const TestLayer = Layer.mergeAll(TestVirusScanPlugin, TestUploadServer);
|
|
60
|
+
|
|
61
|
+
describe("Scan Virus Node", () => {
|
|
62
|
+
describe("Node Creation", () => {
|
|
63
|
+
it.effect("should create scan virus node with correct properties", () =>
|
|
64
|
+
Effect.gen(function* () {
|
|
65
|
+
const node = yield* createScanVirusNode("scan-1", {
|
|
66
|
+
action: "fail",
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
expect(node.id).toBe("scan-1");
|
|
70
|
+
expect(node.name).toBe("Scan Virus");
|
|
71
|
+
expect(node.description).toBe(
|
|
72
|
+
"Scans files for viruses and malware using ClamAV",
|
|
73
|
+
);
|
|
74
|
+
}).pipe(Effect.provide(TestLayer)),
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
it.effect("should create node with default parameters", () =>
|
|
78
|
+
Effect.gen(function* () {
|
|
79
|
+
const node = yield* createScanVirusNode("scan-default");
|
|
80
|
+
|
|
81
|
+
expect(node.id).toBe("scan-default");
|
|
82
|
+
expect(node.name).toBe("Scan Virus");
|
|
83
|
+
}).pipe(Effect.provide(TestLayer)),
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe("Clean File Scanning", () => {
|
|
88
|
+
it.effect("should pass clean file through unchanged", () =>
|
|
89
|
+
Effect.gen(function* () {
|
|
90
|
+
const node = yield* createScanVirusNode("scan-clean", {
|
|
91
|
+
action: "fail",
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const testFile = createTestUploadFile();
|
|
95
|
+
const cleanBytes = createCleanFileBytes();
|
|
96
|
+
|
|
97
|
+
const result = yield* node.run({
|
|
98
|
+
data: testFile,
|
|
99
|
+
jobId: "test-job",
|
|
100
|
+
flowId: "test-flow",
|
|
101
|
+
storageId: "test-storage",
|
|
102
|
+
clientId: "test-client",
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
expect(result.type).toBe("complete");
|
|
106
|
+
if (result.type === "complete") {
|
|
107
|
+
expect(result.data).toBeDefined();
|
|
108
|
+
expect(result.data.metadata?.virusScan).toBeDefined();
|
|
109
|
+
expect(result.data.metadata?.virusScan?.isClean).toBe(true);
|
|
110
|
+
expect(result.data.metadata?.virusScan?.scanned).toBe(true);
|
|
111
|
+
expect(result.data.metadata?.virusScan?.detectedViruses).toEqual([]);
|
|
112
|
+
}
|
|
113
|
+
}).pipe(Effect.provide(TestLayer)),
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
it.effect("should add virus scan metadata to clean file", () =>
|
|
117
|
+
Effect.gen(function* () {
|
|
118
|
+
const node = yield* createScanVirusNode("scan-metadata", {
|
|
119
|
+
action: "fail",
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const testFile = createTestUploadFile();
|
|
123
|
+
|
|
124
|
+
const result = yield* node.run({
|
|
125
|
+
data: testFile,
|
|
126
|
+
jobId: "test-job",
|
|
127
|
+
flowId: "test-flow",
|
|
128
|
+
storageId: "test-storage",
|
|
129
|
+
clientId: "test-client",
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
expect(result.type).toBe("complete");
|
|
133
|
+
if (result.type === "complete") {
|
|
134
|
+
const scanMetadata = result.data.metadata?.virusScan;
|
|
135
|
+
expect(scanMetadata).toBeDefined();
|
|
136
|
+
expect(scanMetadata?.scanned).toBe(true);
|
|
137
|
+
expect(scanMetadata?.isClean).toBe(true);
|
|
138
|
+
expect(scanMetadata?.detectedViruses).toEqual([]);
|
|
139
|
+
expect(scanMetadata?.scanDate).toBeDefined();
|
|
140
|
+
expect(scanMetadata?.engineVersion).toBeDefined();
|
|
141
|
+
expect(scanMetadata?.definitionsDate).toBeDefined();
|
|
142
|
+
}
|
|
143
|
+
}).pipe(Effect.provide(TestLayer)),
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
it.effect("should preserve existing file metadata", () =>
|
|
147
|
+
Effect.gen(function* () {
|
|
148
|
+
const node = yield* createScanVirusNode("scan-preserve", {
|
|
149
|
+
action: "fail",
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const testFile = createTestUploadFile({
|
|
153
|
+
metadata: {
|
|
154
|
+
mimeType: "image/jpeg",
|
|
155
|
+
originalName: "photo.jpg",
|
|
156
|
+
fileName: "photo.jpg",
|
|
157
|
+
extension: "jpg",
|
|
158
|
+
width: 1920,
|
|
159
|
+
height: 1080,
|
|
160
|
+
customField: "custom value",
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const result = yield* node.run({
|
|
165
|
+
data: testFile,
|
|
166
|
+
jobId: "test-job",
|
|
167
|
+
flowId: "test-flow",
|
|
168
|
+
storageId: "test-storage",
|
|
169
|
+
clientId: "test-client",
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
expect(result.type).toBe("complete");
|
|
173
|
+
if (result.type === "complete") {
|
|
174
|
+
expect(result.data.metadata?.mimeType).toBe("image/jpeg");
|
|
175
|
+
expect(result.data.metadata?.width).toBe(1920);
|
|
176
|
+
expect(result.data.metadata?.height).toBe(1080);
|
|
177
|
+
expect(result.data.metadata?.customField).toBe("custom value");
|
|
178
|
+
expect(result.data.metadata?.virusScan).toBeDefined();
|
|
179
|
+
}
|
|
180
|
+
}).pipe(Effect.provide(TestLayer)),
|
|
181
|
+
);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe("Infected File Scanning - Fail Action", () => {
|
|
185
|
+
it.effect(
|
|
186
|
+
"should fail flow when virus detected with fail action",
|
|
187
|
+
() =>
|
|
188
|
+
Effect.gen(function* () {
|
|
189
|
+
const node = yield* createScanVirusNode("scan-fail", {
|
|
190
|
+
action: "fail",
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const testFile = createTestUploadFile();
|
|
194
|
+
|
|
195
|
+
const result = yield* Effect.either(
|
|
196
|
+
node.run({
|
|
197
|
+
data: testFile,
|
|
198
|
+
jobId: "test-job",
|
|
199
|
+
flowId: "test-flow",
|
|
200
|
+
storageId: "test-storage",
|
|
201
|
+
clientId: "test-client",
|
|
202
|
+
}),
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
expect(result._tag).toBe("Left");
|
|
206
|
+
if (result._tag === "Left") {
|
|
207
|
+
expect(result.left).toBeInstanceOf(UploadistaError);
|
|
208
|
+
expect(result.left.code).toBe("VIRUS_DETECTED");
|
|
209
|
+
}
|
|
210
|
+
}).pipe(Effect.provide(TestLayer)),
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
it.effect("should include virus names in error message", () =>
|
|
214
|
+
Effect.gen(function* () {
|
|
215
|
+
const node = yield* createScanVirusNode("scan-names", {
|
|
216
|
+
action: "fail",
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
const testFile = createTestUploadFile();
|
|
220
|
+
|
|
221
|
+
const result = yield* Effect.either(
|
|
222
|
+
node.run({
|
|
223
|
+
data: testFile,
|
|
224
|
+
jobId: "test-job",
|
|
225
|
+
flowId: "test-flow",
|
|
226
|
+
storageId: "test-storage",
|
|
227
|
+
clientId: "test-client",
|
|
228
|
+
}),
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
expect(result._tag).toBe("Left");
|
|
232
|
+
if (result._tag === "Left") {
|
|
233
|
+
const error = result.left as UploadistaError;
|
|
234
|
+
expect(error.body).toBeDefined();
|
|
235
|
+
expect(typeof error.body).toBe("string");
|
|
236
|
+
}
|
|
237
|
+
}).pipe(Effect.provide(TestLayer)),
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
it.effect("should include scan metadata in error details", () =>
|
|
241
|
+
Effect.gen(function* () {
|
|
242
|
+
const node = yield* createScanVirusNode("scan-details", {
|
|
243
|
+
action: "fail",
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
const testFile = createTestUploadFile();
|
|
247
|
+
|
|
248
|
+
const result = yield* Effect.either(
|
|
249
|
+
node.run({
|
|
250
|
+
data: testFile,
|
|
251
|
+
jobId: "test-job",
|
|
252
|
+
flowId: "test-flow",
|
|
253
|
+
storageId: "test-storage",
|
|
254
|
+
clientId: "test-client",
|
|
255
|
+
}),
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
expect(result._tag).toBe("Left");
|
|
259
|
+
if (result._tag === "Left") {
|
|
260
|
+
const error = result.left as UploadistaError;
|
|
261
|
+
expect(error.details).toBeDefined();
|
|
262
|
+
expect(error.details?.scanMetadata).toBeDefined();
|
|
263
|
+
expect(error.details?.scanMetadata.isClean).toBe(false);
|
|
264
|
+
expect(error.details?.scanMetadata.detectedViruses).toBeDefined();
|
|
265
|
+
expect(error.details?.scanMetadata.detectedViruses.length).toBeGreaterThan(
|
|
266
|
+
0,
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
}).pipe(Effect.provide(TestLayer)),
|
|
270
|
+
);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
describe("Infected File Scanning - Pass Action", () => {
|
|
274
|
+
it.effect(
|
|
275
|
+
"should continue flow when virus detected with pass action",
|
|
276
|
+
() =>
|
|
277
|
+
Effect.gen(function* () {
|
|
278
|
+
const node = yield* createScanVirusNode("scan-pass", {
|
|
279
|
+
action: "pass",
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
const testFile = createTestUploadFile();
|
|
283
|
+
|
|
284
|
+
const result = yield* node.run({
|
|
285
|
+
data: testFile,
|
|
286
|
+
jobId: "test-job",
|
|
287
|
+
flowId: "test-flow",
|
|
288
|
+
storageId: "test-storage",
|
|
289
|
+
clientId: "test-client",
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
expect(result.type).toBe("complete");
|
|
293
|
+
}).pipe(Effect.provide(TestLayer)),
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
it.effect(
|
|
297
|
+
"should add virus detection metadata when passing infected file",
|
|
298
|
+
() =>
|
|
299
|
+
Effect.gen(function* () {
|
|
300
|
+
const node = yield* createScanVirusNode("scan-pass-metadata", {
|
|
301
|
+
action: "pass",
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
const testFile = createTestUploadFile();
|
|
305
|
+
|
|
306
|
+
const result = yield* node.run({
|
|
307
|
+
data: testFile,
|
|
308
|
+
jobId: "test-job",
|
|
309
|
+
flowId: "test-flow",
|
|
310
|
+
storageId: "test-storage",
|
|
311
|
+
clientId: "test-client",
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
expect(result.type).toBe("complete");
|
|
315
|
+
if (result.type === "complete") {
|
|
316
|
+
const scanMetadata = result.data.metadata?.virusScan;
|
|
317
|
+
expect(scanMetadata).toBeDefined();
|
|
318
|
+
expect(scanMetadata?.scanned).toBe(true);
|
|
319
|
+
expect(scanMetadata?.isClean).toBe(false);
|
|
320
|
+
expect(scanMetadata?.detectedViruses).toBeDefined();
|
|
321
|
+
expect(scanMetadata?.detectedViruses.length).toBeGreaterThan(0);
|
|
322
|
+
}
|
|
323
|
+
}).pipe(Effect.provide(TestLayer)),
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
it.effect("should preserve file bytes when passing infected file", () =>
|
|
327
|
+
Effect.gen(function* () {
|
|
328
|
+
const node = yield* createScanVirusNode("scan-pass-bytes", {
|
|
329
|
+
action: "pass",
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
const testFile = createTestUploadFile();
|
|
333
|
+
|
|
334
|
+
const result = yield* node.run({
|
|
335
|
+
data: testFile,
|
|
336
|
+
jobId: "test-job",
|
|
337
|
+
flowId: "test-flow",
|
|
338
|
+
storageId: "test-storage",
|
|
339
|
+
clientId: "test-client",
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
expect(result.type).toBe("complete");
|
|
343
|
+
if (result.type === "complete") {
|
|
344
|
+
expect(result.data).toBeDefined();
|
|
345
|
+
expect(result.data.id).toBe(testFile.id);
|
|
346
|
+
}
|
|
347
|
+
}).pipe(Effect.provide(TestLayer)),
|
|
348
|
+
);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
describe("Parameter Validation", () => {
|
|
352
|
+
it.effect("should accept valid fail action", () =>
|
|
353
|
+
Effect.gen(function* () {
|
|
354
|
+
const node = yield* createScanVirusNode("scan-valid-fail", {
|
|
355
|
+
action: "fail",
|
|
356
|
+
timeout: 60000,
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
expect(node.id).toBe("scan-valid-fail");
|
|
360
|
+
}).pipe(Effect.provide(TestLayer)),
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
it.effect("should accept valid pass action", () =>
|
|
364
|
+
Effect.gen(function* () {
|
|
365
|
+
const node = yield* createScanVirusNode("scan-valid-pass", {
|
|
366
|
+
action: "pass",
|
|
367
|
+
timeout: 120000,
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
expect(node.id).toBe("scan-valid-pass");
|
|
371
|
+
}).pipe(Effect.provide(TestLayer)),
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
it.effect("should apply default timeout", () =>
|
|
375
|
+
Effect.gen(function* () {
|
|
376
|
+
const node = yield* createScanVirusNode("scan-default-timeout", {
|
|
377
|
+
action: "fail",
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
expect(node.id).toBe("scan-default-timeout");
|
|
381
|
+
}).pipe(Effect.provide(TestLayer)),
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
it.effect("should accept custom timeout within limits", () =>
|
|
385
|
+
Effect.gen(function* () {
|
|
386
|
+
const node = yield* createScanVirusNode("scan-custom-timeout", {
|
|
387
|
+
action: "fail",
|
|
388
|
+
timeout: 180000, // 3 minutes
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
expect(node.id).toBe("scan-custom-timeout");
|
|
392
|
+
}).pipe(Effect.provide(TestLayer)),
|
|
393
|
+
);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
describe("Engine Version", () => {
|
|
397
|
+
it.effect("should include engine version in scan metadata", () =>
|
|
398
|
+
Effect.gen(function* () {
|
|
399
|
+
const node = yield* createScanVirusNode("scan-version", {
|
|
400
|
+
action: "fail",
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
const testFile = createTestUploadFile();
|
|
404
|
+
|
|
405
|
+
const result = yield* node.run({
|
|
406
|
+
data: testFile,
|
|
407
|
+
jobId: "test-job",
|
|
408
|
+
flowId: "test-flow",
|
|
409
|
+
storageId: "test-storage",
|
|
410
|
+
clientId: "test-client",
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
expect(result.type).toBe("complete");
|
|
414
|
+
if (result.type === "complete") {
|
|
415
|
+
const scanMetadata = result.data.metadata?.virusScan;
|
|
416
|
+
expect(scanMetadata?.engineVersion).toBeDefined();
|
|
417
|
+
expect(typeof scanMetadata?.engineVersion).toBe("string");
|
|
418
|
+
expect(scanMetadata?.engineVersion.length).toBeGreaterThan(0);
|
|
419
|
+
}
|
|
420
|
+
}).pipe(Effect.provide(TestLayer)),
|
|
421
|
+
);
|
|
422
|
+
});
|
|
423
|
+
});
|