Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Typescript/add for stream method #358

Merged
merged 11 commits into from
Feb 22, 2024
35 changes: 29 additions & 6 deletions .github/workflows/ci-typescript.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,17 @@ on:
options:
- production
- staging
- develop
default: "production"

env:
CLIENT_ID: ${{ secrets.CLIENT_ID }}
CLIENT_SECRET: ${{ (inputs.environment == 'production' || inputs.environment == null || startsWith(github.ref, 'refs/tags/ts')) && secrets.CLIENT_SECRET || secrets.STAGING_CLIENT_SECRET }}
VAAS_URL: ${{ (inputs.environment == 'production' || inputs.environment == null || startsWith(github.ref, 'refs/tags/ts')) && 'wss://gateway.production.vaas.gdatasecurity.de' || 'wss://gateway.staging.vaas.gdatasecurity.de' }}
TOKEN_URL: ${{ (inputs.environment == 'production' || inputs.environment == null || startsWith(github.ref, 'refs/tags/ts')) && 'https://account.gdata.de/realms/vaas-production/protocol/openid-connect/token' || 'https://account-staging.gdata.de/realms/vaas-staging/protocol/openid-connect/token' }}
CLIENT_SECRET: ${{secrets.CLIENT_SECRET}}
VAAS_URL: 'wss://gateway.production.vaas.gdatasecurity.de'
TOKEN_URL: 'https://account.gdata.de/realms/vaas-production/protocol/openid-connect/token'
VAAS_CLIENT_ID: ${{ secrets.VAAS_CLIENT_ID }}
VAAS_USER_NAME: ${{ secrets.VAAS_USER_NAME }}
VAAS_PASSWORD: ${{ (inputs.environment == 'production' || inputs.environment == null || startsWith(github.ref, 'refs/tags/ts')) && secrets.VAAS_PASSWORD || secrets.STAGING_VAAS_PASSWORD }}
VAAS_PASSWORD: ${{secrets.VAAS_PASSWORD}}

jobs:
build-typescript:
Expand All @@ -42,14 +43,36 @@ jobs:
- name: checkout
uses: actions/checkout@v3

- uses: pnpm/action-setup@v2
- name: set staging environment
if: (inputs.environment == 'staging' || (startsWith(github.ref, 'refs/tags/ts') && endsWith(github.ref, '-beta')))
run: |
echo "CLIENT_ID=${{ secrets.STAGING_CLIENT_ID }}" >> $GITHUB_ENV
echo "CLIENT_SECRET=${{ secrets.STAGING_CLIENT_SECRET }}" >> $GITHUB_ENV
echo "VAAS_URL=wss://gateway.staging.vaas.gdatasecurity.de" >> $GITHUB_ENV
echo "TOKEN_URL=https://account-staging.gdata.de/realms/vaas-staging/protocol/openid-connect/token" >> $GITHUB_ENV
echo "VAAS_CLIENT_ID=${{ secrets.STAGING_VAAS_CLIENT_ID }}" >> $GITHUB_ENV
echo "VAAS_USER_NAME=${{ secrets.STAGING_VAAS_USER_NAME }}" >> $GITHUB_ENV
echo "VAAS_PASSWORD=${{ secrets.STAGING_VAAS_PASSWORD }}" >> $GITHUB_ENV

- name: set develop environment
if: (inputs.environment == 'develop' || (startsWith(github.ref, 'refs/tags/ts') && endsWith(github.ref, '-alpha')))
run: |
echo "CLIENT_ID=${{ secrets.DEVELOP_CLIENT_ID }}" >> $GITHUB_ENV
echo "CLIENT_SECRET=${{ secrets.DEVELOP_CLIENT_SECRET }}" >> $GITHUB_ENV
echo "VAAS_URL=wss://gateway.develop.vaas.gdatasecurity.de" >> $GITHUB_ENV
echo "TOKEN_URL=https://account-staging.gdata.de/realms/vaas-develop/protocol/openid-connect/token" >> $GITHUB_ENV
echo "VAAS_CLIENT_ID=${{ secrets.DEVELOP_VAAS_CLIENT_ID }}" >> $GITHUB_ENV
echo "VAAS_USER_NAME=${{ secrets.DEVELOP_VAAS_USER_NAME }}" >> $GITHUB_ENV
echo "VAAS_PASSWORD=${{ secrets.DEVELOP_VAAS_PASSWORD }}" >> $GITHUB_ENV

- uses: pnpm/action-setup@v3
name: install pnpm
id: pnpm-install
with:
version: 8
run_install: false

- uses: actions/setup-node@v3
- uses: actions/setup-node@v4
name: setup node
with:
node-version: 20
Expand Down
2 changes: 1 addition & 1 deletion typescript/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,4 @@
"test": "TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\" }' mocha --exit -r ts-node/register 'tests/**/*.ts'",
"format": "prettier --write src/** tests/**"
}
}
}
65 changes: 61 additions & 4 deletions typescript/src/Vaas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,13 @@ import {
VaasAuthenticationError,
VaasConnectionClosedError,
VaasInvalidStateError,
VaasServerError,
VaasTimeoutError,
} from "./VaasErrors";
import { VaasVerdict } from "./messages/vaas_verdict";
import { VerdictRequestForUrl } from "./messages/verdict_request_for_url";
import { VerdictRequestForStream } from "./messages/verdict_request_for_stream";
import { Readable } from "stream";

const VAAS_URL = "wss://gateway.production.vaas.gdatasecurity.de";
const defaultSerializer = new JsonSerializer();
Expand Down Expand Up @@ -57,6 +60,8 @@ export class Vaas {

defaultTimeoutHashReq: number = 2_000;
defaultTimeoutFileReq: number = 600_000;
defaultTimeoutUrlReq: number = 600_000;
defaultTimeoutStreamReq: number = 100_000;
debug = false;

constructor(private webSocketFactory = (url: string) => new WebSocket(url)) {
Expand All @@ -69,6 +74,25 @@ export class Vaas {
}).join("");
}

/** Get verdict for Readable Stream
* @throws {VaasInvalidStateError} If connect() has not been called and awaited. Signifies caller error.
* @throws {VaasAuthenticationError} Authentication failed.
* @throws {VaasConnectionClosedError} Connection was closed. Call connect() to reconnect.
* @throws {VaasTimeoutError} Timeout. Retry request.
* @throws {VaasServerError} Stream could not be read properly in VaaS clouds
*/
public async forStream(
stream: Readable,
ct: CancellationToken = CancellationToken.fromMilliseconds(
this.defaultTimeoutStreamReq
)
): Promise<VaasVerdict> {
const request = this.forStreamRequest(stream).then(
(response) => response
);
return timeout(request, ct.timeout());
}

/** Get verdict for URL
* @throws {VaasInvalidStateError} If connect() has not been called and awaited. Signifies caller error.
* @throws {VaasAuthenticationError} Authentication failed.
Expand All @@ -78,7 +102,7 @@ export class Vaas {
public async forUrl(
url: URL,
ct: CancellationToken = CancellationToken.fromMilliseconds(
this.defaultTimeoutHashReq
this.defaultTimeoutUrlReq
)
): Promise<VaasVerdict> {
const request = this.forUrlRequest(url).then(
Expand Down Expand Up @@ -215,6 +239,38 @@ export class Vaas {
ws.send(verdictReq);
});
}

private async forStreamRequest(
stream: Readable
): Promise<VaasVerdict> {
const ws = this.getAuthenticatedWebSocket();
return new Promise((resolve, reject) => {
const guid = uuidv4();
if (this.debug) console.debug("uuid", guid);
this.verdictPromises.set(guid, {
resolve: async (verdictResponse: VerdictResponse) => {
var contentLength;
if (verdictResponse.verdict === Verdict.UNKNOWN) {
contentLength = stream.readableLength;
await this.upload(verdictResponse, stream, contentLength);
this.verdictPromises.delete(guid);
}
if (verdictResponse.verdict !== Verdict.UNKNOWN && contentLength === 0) {
throw new VaasServerError("Server returned verdict without receiving content");
}
resolve(new VaasVerdict(verdictResponse.sha256, verdictResponse.verdict));
doxthree marked this conversation as resolved.
Show resolved Hide resolved
},
reject: (reason) => reject(reason),
});

const verdictReq = JSON.stringify(
defaultSerializer.serialize(
new VerdictRequestForStream(guid, this.connection!.sessionId as string)
)
);
ws.send(verdictReq);
});
}

/** Connect to VaaS
* @throws {VaasAuthenticationError} Authentication failed.
Expand Down Expand Up @@ -303,7 +359,8 @@ export class Vaas {

private async upload(
verdictResponse: VerdictResponse,
fileBuffer: Uint8Array
input: Uint8Array | Readable,
contentLength: number = Infinity
) {
return new Promise(async (resolve, reject) => {
const instance = axios.default.create({
Expand All @@ -314,10 +371,10 @@ export class Vaas {
Authorization: verdictResponse.upload_token!,
"Content-Type": "application/octet-stream"
},
maxBodyLength: Infinity,
maxBodyLength: contentLength,
});
await instance
.put("/", fileBuffer)
.put("/", input)
.then((response) => resolve(response))
.catch((error) => {
if (error instanceof axios.AxiosError && error.response) {
Expand Down
9 changes: 9 additions & 0 deletions typescript/src/VaasErrors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,12 @@ export class VaasTimeoutError extends Error {
super("Timeout");
}
}

/** Vaas server error.
* @description These are coding errors and be prevented by the developer.
*/
export class VaasServerError extends Error {
constructor(message: string) {
super(message);
}
}
3 changes: 2 additions & 1 deletion typescript/src/messages/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ export enum Kind {
DetectionResponse = "DetectionResponse",
PersistedRequest = "PersistedRequest",
SampleProcessingResponse = "SampleProcessingResponse",
VerdictRequestForUrl = "VerdictRequestForUrl"
VerdictRequestForUrl = "VerdictRequestForUrl",
VerdictRequestForStream = "VerdictRequestForStream"
}

@JsonObject()
Expand Down
14 changes: 14 additions & 0 deletions typescript/src/messages/verdict_request_for_stream.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { JsonProperty, JsonObject } from "typescript-json-serializer";
import { Kind, Message } from "./message";

@JsonObject()
export class VerdictRequestForStream extends Message {
public constructor(guid: string, session_id: string) {
super(Kind.VerdictRequestForStream);
this.session_id = session_id;
this.guid = guid;
}

@JsonProperty() public guid: string;
@JsonProperty() public session_id: string;
}
32 changes: 30 additions & 2 deletions typescript/tests/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {
} from "../src/VaasErrors";
import ClientCredentialsGrantAuthenticator from "../src/ClientCredentialsGrantAuthenticator";
import ResourceOwnerPasswordGrantAuthenticator from "../src/ResourceOwnerPasswordGrantAuthenticator";
import { Readable } from "stream";
import axios, { AxiosResponse } from "axios";

const chai = require("chai");
const chaiAsPromised = require("chai-as-promised");
Expand Down Expand Up @@ -203,7 +205,7 @@ describe("Test verdict requests", function () {
expect(verdict2.verdict).to.equal("Clean");
expect(verdict2.sha256.toUpperCase()).to.equal("3A78F382E8E2968EC201B33178102E06DB72E4F2D1505E058A4613C1E977825C");
});

https://www.virustotal.com/gui/file/edb6991d68ba5c7ed43f198c3d2593c770f2634beeb8c83afe3138279e5e81f3
xit("keeps connection alive", async () => {
const vaas = await createVaasWithClientCredentialsGrantAuthenticator();
const sha256 =
Expand Down Expand Up @@ -231,11 +233,37 @@ describe("Test verdict requests", function () {
expect(verdict.verdict).to.equal("Clean");
});

it('if EICAR url is submitted, a verdict "clean" is expected', async () => {
it('if EICAR url is submitted, a verdict "malicious" is expected', async () => {
const vaas = await createVaasWithClientCredentialsGrantAuthenticator();
const verdict = await vaas.forUrl(new URL("https://secure.eicar.org/eicar.com"));
expect(verdict.verdict).to.equal("Malicious");
});

it('if a clean stream is submitted, a verdict "clean" is expected', async () => {
const vaas = await createVaasWithClientCredentialsGrantAuthenticator();
const stream = new Readable();
stream.push("I am Clean");
stream.push(null);
const verdict = await vaas.forStream(stream);
expect(verdict.verdict).to.equal("Clean");
});

it('if a EICAR stream is submitted, a verdict "malicious" is expected', async () => {
const vaas = await createVaasWithClientCredentialsGrantAuthenticator();
const stream = new Readable();
stream._read = () => {};
stream.push(`X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*`);
stream.push(null);
const verdict = await vaas.forStream(stream);
expect(verdict.verdict).to.equal("Malicious");
});

it('if a EICAR stream from an url is submitted, a verdict "malicious" is expected', async () => {
const vaas = await createVaasWithClientCredentialsGrantAuthenticator();
const response = await axios.get<Readable>("https://secure.eicar.org/eicar.com.txt", { responseType: "stream" });
const verdict = await vaas.forStream(response.data);
expect(verdict.verdict).to.equal("Malicious");
});
});

describe("Vaas", async () => {
Expand Down
Loading