From 04fdd9af83bf7f36e8c4857a03a3144feaf93ece Mon Sep 17 00:00:00 2001 From: Lennart Dohmann Date: Tue, 20 Feb 2024 14:29:09 +0100 Subject: [PATCH 1/6] WIP ForStream untested --- php/src/vaas/Message/Kind.php | 4 + .../vaas/Message/VerdictRequestForStream.php | 19 +++++ php/src/vaas/Vaas.php | 83 +++++++++++++++++++ ...lientCredentialsGrantAuthenticatorTest.php | 7 ++ php/tests/vaas/composer.json | 2 +- 5 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 php/src/vaas/Message/VerdictRequestForStream.php diff --git a/php/src/vaas/Message/Kind.php b/php/src/vaas/Message/Kind.php index a3757699..65cb55ce 100644 --- a/php/src/vaas/Message/Kind.php +++ b/php/src/vaas/Message/Kind.php @@ -12,6 +12,7 @@ class Kind implements JsonSerializable public const VERDICT_REQUEST = "VerdictRequest"; public const VERDICT_RESPONSE = "VerdictResponse"; public const VERDICT_REQUEST_FOR_URL = "VerdictRequestForUrl"; + public const VERDICT_REQUEST_FOR_STREAM = "VerdictRequestForStream"; public const ERROR = "Error"; private string $_kindString = ""; @@ -34,6 +35,9 @@ public function __construct(string $type) case self::VERDICT_REQUEST_FOR_URL: $this->_kindString = self::VERDICT_REQUEST_FOR_URL; break; + case self::VERDICT_REQUEST_FOR_STREAM: + $this->_kindString = self::VERDICT_REQUEST_FOR_STREAM; + break; case self::ERROR: $this->_kindString = self::ERROR; break; diff --git a/php/src/vaas/Message/VerdictRequestForStream.php b/php/src/vaas/Message/VerdictRequestForStream.php new file mode 100644 index 00000000..1344914f --- /dev/null +++ b/php/src/vaas/Message/VerdictRequestForStream.php @@ -0,0 +1,19 @@ +kind = new Kind(Kind::VERDICT_REQUEST_FOR_URL); + $this->guid = $uuid != null ? $uuid : UuidV4::getFactory()->uuid4()->toString(); + $this->session_id = $SessionId; + } +} diff --git a/php/src/vaas/Vaas.php b/php/src/vaas/Vaas.php index 8ff15573..87d490ed 100644 --- a/php/src/vaas/Vaas.php +++ b/php/src/vaas/Vaas.php @@ -3,9 +3,12 @@ namespace VaasSdk; use GuzzleHttp\Client as HttpClient; +use GuzzleHttp\Exception\GuzzleException; +use GuzzleHttp\Psr7\Stream; use InvalidArgumentException; use JsonMapper; use JsonMapper_Exception; +use Ramsey\Uuid\Rfc4122\UuidV4; use VaasSdk\Exceptions\TimeoutException; use VaasSdk\Exceptions\UploadFailedException; use VaasSdk\Exceptions\VaasAuthenticationException; @@ -19,11 +22,13 @@ use VaasSdk\Message\Verdict; use VaasSdk\Message\Kind; use VaasSdk\Message\VerdictRequest; +use VaasSdk\Message\VerdictRequestForStream; use VaasSdk\Message\VerdictResponse; use VaasSdk\Message\VerdictRequestForUrl; use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; use VaasSdk\Message\VaasVerdict; +use WebSocket\BadOpcodeException; class Vaas { @@ -154,6 +159,41 @@ public function ForFile(string $path, bool $upload = true, string $uuid = null): return new VaasVerdict($verdictResponse); } + /** + * @throws JsonMapper_Exception + * @throws VaasClientException + * @throws TimeoutException + * @throws VaasServerException + * @throws BadOpcodeException + * @throws VaasInvalidStateException + * @throws GuzzleException + * @throws UploadFailedException + */ + public function ForStream(Stream $stream, string $uuid=null): VaasVerdict + { + if ($uuid == null){ + $uuid = UuidV4::getFactory()->uuid4()->toString(); + } + + $verdictResponse = $this->_verdictResponseForStream($uuid); + + if ($verdictResponse->verdict != Verdict::UNKNOWN){ + throw new VaasServerException("Server returned verdict without receiving content."); + } + if ($verdictResponse->upload_token == null || $verdictResponse->upload_token == ""){ + throw new JsonMapper_Exception("VerdictResponse missing UploadToken for stream upload."); + } + if ($verdictResponse->url == null || $verdictResponse->url == ""){ + throw new JsonMapper_Exception("VerdictResponse missing URL for stream upload."); + } + + $this->UploadStream($stream, $verdictResponse->url, $verdictResponse->upload_token); + + $verdictResponse = $this->_waitForVerdict($uuid); + + return new VaasVerdict($verdictResponse); + } + /** * @return AuthResponse * @throws VaasConnectionClosedException @@ -349,6 +389,33 @@ private function _verdictResponseForUrl(string $url, string $uuid = null): Verdi return $this->_waitForVerdict($request->guid); } + /** + * @throws JsonMapper_Exception + * @throws VaasClientException + * @throws TimeoutException + * @throws VaasServerException + * @throws BadOpcodeException + * @throws VaasInvalidStateException + */ + private function _verdictResponseForStream(string $uuid=null): VerdictResponse + { + if ($this->_logger != null) + $this->_logger->debug("_verdictResponseForStream"); + + if (!isset($this->_vaasConnection)) { + throw new VaasInvalidStateException("connect() was not called"); + } + $websocket = $this->_vaasConnection->GetAuthenticatedWebsocket(); + + $request = new VerdictRequestForStream($uuid, $this->_vaasConnection->SessionId); + $websocket->send(json_encode($request)); + + if ($this->_logger != null) + $this->_logger->debug("verdictResponse", ["VerdictResponse" => json_encode($request)]); + + return $this->_waitForVerdict($request->guid); + } + /** * Sets the timeout in seconds the websocket client can take for one receive * @@ -386,4 +453,20 @@ public function setUploadTimeout(int $UploadTimeoutInSeconds): self $this->_uploadTimeoutInSeconds = $UploadTimeoutInSeconds; return $this; } + + /** + * @throws GuzzleException + * @throws UploadFailedException + */ + private function UploadStream(Stream $stream, string $url, string $uploadToken) + { + $response = $this->_httpClient->put($url, [ + 'body' => $stream, + 'timeout' => $this->_uploadTimeoutInSeconds, + 'headers' => ["Authorization" => $uploadToken] + ]); + if ($response->getStatusCode() > 399) { + throw new UploadFailedException($response->getReasonPhrase(), $response->getStatusCode()); + } + } } diff --git a/php/tests/vaas/ClientCredentialsGrantAuthenticatorTest.php b/php/tests/vaas/ClientCredentialsGrantAuthenticatorTest.php index a33f9307..bf07a099 100644 --- a/php/tests/vaas/ClientCredentialsGrantAuthenticatorTest.php +++ b/php/tests/vaas/ClientCredentialsGrantAuthenticatorTest.php @@ -39,4 +39,11 @@ public function testAuthenticatorWithInvalidCredentials_ThrowsAccessDeniedExcept $authenticator = new ClientCredentialsGrantAuthenticator("invalid", "invalid", $_ENV["TOKEN_URL"]); $authenticator->getToken(); } + + public function testAuthenticatorWithValidCredentials_ReturnsToken(): void + { + $authenticator = new ClientCredentialsGrantAuthenticator($_ENV["CLIENT_ID"], $_ENV["CLIENT_SECRET"], $_ENV["TOKEN_URL"]); + $token = $authenticator->getToken(); + $this->assertNotEmpty($token); + } } \ No newline at end of file diff --git a/php/tests/vaas/composer.json b/php/tests/vaas/composer.json index ea590a08..a9faffbc 100644 --- a/php/tests/vaas/composer.json +++ b/php/tests/vaas/composer.json @@ -17,7 +17,7 @@ "league/oauth2-client": "^2.4.0" }, "require-dev": { - "phpunit/phpunit": "^9", + "phpunit/phpunit": "^10", "phpspec/prophecy-phpunit": "^2" } } \ No newline at end of file From 57353c5cc8474c6cb8c28933326a54da3f003c3b Mon Sep 17 00:00:00 2001 From: Lennart Dohmann Date: Tue, 20 Feb 2024 17:32:43 +0100 Subject: [PATCH 2/6] Add unit tests --- .../vaas/Message/VerdictRequestForStream.php | 2 +- php/src/vaas/Vaas.php | 2 +- php/tests/vaas/VaasTest.php | 108 ++++++++++++++++++ 3 files changed, 110 insertions(+), 2 deletions(-) diff --git a/php/src/vaas/Message/VerdictRequestForStream.php b/php/src/vaas/Message/VerdictRequestForStream.php index 1344914f..96fbb1e9 100644 --- a/php/src/vaas/Message/VerdictRequestForStream.php +++ b/php/src/vaas/Message/VerdictRequestForStream.php @@ -12,7 +12,7 @@ class VerdictRequestForStream public function __construct(string $SessionId, string $uuid = null) { - $this->kind = new Kind(Kind::VERDICT_REQUEST_FOR_URL); + $this->kind = new Kind(Kind::VERDICT_REQUEST_FOR_STREAM); $this->guid = $uuid != null ? $uuid : UuidV4::getFactory()->uuid4()->toString(); $this->session_id = $SessionId; } diff --git a/php/src/vaas/Vaas.php b/php/src/vaas/Vaas.php index 87d490ed..4479c668 100644 --- a/php/src/vaas/Vaas.php +++ b/php/src/vaas/Vaas.php @@ -407,7 +407,7 @@ private function _verdictResponseForStream(string $uuid=null): VerdictResponse } $websocket = $this->_vaasConnection->GetAuthenticatedWebsocket(); - $request = new VerdictRequestForStream($uuid, $this->_vaasConnection->SessionId); + $request = new VerdictRequestForStream($this->_vaasConnection->SessionId, $uuid); $websocket->send(json_encode($request)); if ($this->_logger != null) diff --git a/php/tests/vaas/VaasTest.php b/php/tests/vaas/VaasTest.php index 0f432937..d57dadc8 100644 --- a/php/tests/vaas/VaasTest.php +++ b/php/tests/vaas/VaasTest.php @@ -4,12 +4,18 @@ require_once __DIR__ . "/vendor/autoload.php"; +use GuzzleHttp\Client; +use GuzzleHttp\Exception\GuzzleException; +use GuzzleHttp\Psr7\Stream; +use JsonMapper_Exception; use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; use VaasSdk\ClientCredentialsGrantAuthenticator; use VaasSdk\Exceptions\TimeoutException; +use VaasSdk\Exceptions\UploadFailedException; use VaasSdk\Exceptions\VaasAuthenticationException; use VaasSdk\Exceptions\VaasClientException; +use VaasSdk\Exceptions\VaasServerException; use VaasSdk\ResourceOwnerPasswordGrantAuthenticator; use VaasSdk\Vaas; use Dotenv\Dotenv; @@ -21,6 +27,7 @@ use VaasSdk\Exceptions\VaasInvalidStateException; use VaasSdk\Message\Verdict; use VaasSdk\Sha256; +use WebSocket\BadOpcodeException; final class VaasTest extends TestCase { @@ -422,4 +429,105 @@ public function testForUrl_WithStatus4xx_ThrowsVaasClientException() $verdict = $vaas->ForUrl($invalidUrl); $this->_getDebugLogger()->info("Verdict for URL " . $invalidUrl . " is " . $verdict->Verdict); } + + /** + * @throws JsonMapper_Exception + * @throws VaasClientException + * @throws VaasServerException + * @throws UploadFailedException + * @throws TimeoutException + * @throws BadOpcodeException + * @throws GuzzleException + * @throws VaasAuthenticationException + * @throws VaasInvalidStateException + */ + public function testForStream_WithEicarString_ReturnsMalicious() + { + $vaas = new Vaas($_ENV["VAAS_URL"], $this->_getDebugLogger()); + $vaas->Connect($this->getClientCredentialsGrantAuthenticator()->getToken()); + $eicar = "X5O!P%@AP[4\\PZX54(P^)7CC)7}\$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!\$H+H*"; + $stream = fopen(sprintf('data://text/plain,%s', $eicar), 'r'); + rewind($stream); + $eicarStream = new Stream($stream); + + $verdict = $vaas->ForStream($eicarStream); + + $this->assertEquals(Verdict::MALICIOUS, $verdict->Verdict); + } + + /** + * @throws JsonMapper_Exception + * @throws VaasClientException + * @throws VaasServerException + * @throws TimeoutException + * @throws UploadFailedException + * @throws GuzzleException + * @throws BadOpcodeException + * @throws VaasInvalidStateException + * @throws VaasAuthenticationException + */ + public function testForStream_WithCleanString_ReturnsClean() + { + $vaas = new Vaas($_ENV["VAAS_URL"], $this->_getDebugLogger()); + $vaas->Connect($this->getClientCredentialsGrantAuthenticator()->getToken()); + $clean = "I am a clean string"; + $stream = fopen(sprintf('data://text/plain,%s', $clean), 'r'); + rewind($stream); + $eicarStream = new Stream($stream); + + $verdict = $vaas->ForStream($eicarStream); + + $this->assertEquals(Verdict::CLEAN, $verdict->Verdict); + } + + /** + * @throws GuzzleException + * @throws JsonMapper_Exception + * * @throws VaasClientException + * * @throws VaasServerException + * * @throws TimeoutException + * * @throws UploadFailedException + * * @throws GuzzleException + * * @throws BadOpcodeException + * * @throws VaasInvalidStateException + * * @throws VaasAuthenticationException + */ + public function testForStream_WithCleanUrlContentAsStream_ReturnsClean() + { + $vaas = new Vaas($_ENV["VAAS_URL"], $this->_getDebugLogger()); + $vaas->Connect($this->getClientCredentialsGrantAuthenticator()->getToken()); + $url = "https://raw.githubusercontent.com/GDATASoftwareAG/vaas/main/Readme.md"; + $httpClient = new Client(); + $response = $httpClient->get($url); + $stream = new Stream($response->getBody()->detach()); + + $verdict = $vaas->ForStream($stream); + + $this->assertEquals(Verdict::CLEAN, $verdict->Verdict); + } + + /** + * @throws GuzzleException + * @throws JsonMapper_Exception + * * @throws VaasClientException + * * @throws VaasServerException + * * @throws TimeoutException + * * @throws UploadFailedException + * * @throws GuzzleException + * * @throws BadOpcodeException + * * @throws VaasInvalidStateException + * * @throws VaasAuthenticationException + */ + public function testForStream_WithEicarUrlContentAsStream_ReturnsMalicious() + { + $vaas = new Vaas($_ENV["VAAS_URL"], $this->_getDebugLogger()); + $vaas->Connect($this->getClientCredentialsGrantAuthenticator()->getToken()); + $httpClient = new Client(); + $response = $httpClient->get(self::MALICIOUS_URL); + $stream = new Stream($response->getBody()->detach()); + + $verdict = $vaas->ForStream($stream); + + $this->assertEquals(Verdict::MALICIOUS, $verdict->Verdict); + } } From 05f881c9fd395379cf9830cb489b3741839ff197 Mon Sep 17 00:00:00 2001 From: Lennart Dohmann Date: Tue, 20 Feb 2024 17:34:10 +0100 Subject: [PATCH 3/6] Add package.xml to gitignore --- php/.gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/php/.gitignore b/php/.gitignore index c8e48944..d20f259f 100644 --- a/php/.gitignore +++ b/php/.gitignore @@ -1 +1,2 @@ -*.lock \ No newline at end of file +*.lock +package.xml \ No newline at end of file From 8c6bbf151451b777c7b1e59b50484da158906b44 Mon Sep 17 00:00:00 2001 From: unglaublicherdude Date: Tue, 20 Feb 2024 21:12:05 +0100 Subject: [PATCH 4/6] just linting that gets applied when using the php 8.3 devcontainer --- php/src/vaas/ClientCredentialsGrantAuthenticator.php | 12 ++++++------ .../vaas/ClientCredentialsGrantAuthenticatorTest.php | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/php/src/vaas/ClientCredentialsGrantAuthenticator.php b/php/src/vaas/ClientCredentialsGrantAuthenticator.php index acdaabfd..e28e6cd1 100644 --- a/php/src/vaas/ClientCredentialsGrantAuthenticator.php +++ b/php/src/vaas/ClientCredentialsGrantAuthenticator.php @@ -14,9 +14,10 @@ class ClientCredentialsGrantAuthenticator private HttpClient $_httpClient; public function __construct( - string $clientId, string $clientSecret, - string $tokenEndpoint = "https://account.gdata.de/realms/vaas-production/protocol/openid-connect/token") - { + string $clientId, + string $clientSecret, + string $tokenEndpoint = "https://account.gdata.de/realms/vaas-production/protocol/openid-connect/token" + ) { $this->_clientId = $clientId; $this->_clientSecret = $clientSecret; $this->_tokenEndpoint = $tokenEndpoint; @@ -43,11 +44,10 @@ public function getToken(): string if ($response->getStatusCode() != 200) { throw new VaasAuthenticationException($response->getReasonPhrase(), $response->getStatusCode()); } - } - catch (ClientException $e) { + } catch (ClientException $e) { throw new VaasAuthenticationException($e->getMessage(), $e->getCode()); } $response_body = json_decode($response->getBody()); return $response_body->access_token; } -} \ No newline at end of file +} diff --git a/php/tests/vaas/ClientCredentialsGrantAuthenticatorTest.php b/php/tests/vaas/ClientCredentialsGrantAuthenticatorTest.php index bf07a099..31b903eb 100644 --- a/php/tests/vaas/ClientCredentialsGrantAuthenticatorTest.php +++ b/php/tests/vaas/ClientCredentialsGrantAuthenticatorTest.php @@ -46,4 +46,4 @@ public function testAuthenticatorWithValidCredentials_ReturnsToken(): void $token = $authenticator->getToken(); $this->assertNotEmpty($token); } -} \ No newline at end of file +} From 681969e455dbea4c85aa657e824f70f1bef4838a Mon Sep 17 00:00:00 2001 From: unglaublicherdude Date: Tue, 20 Feb 2024 21:14:30 +0100 Subject: [PATCH 5/6] makes the url parameter nullable and adjusts the exception to the one that actually is thrown --- php/src/vaas/Vaas.php | 22 +++++++++++----------- php/tests/vaas/VaasTest.php | 8 ++++---- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/php/src/vaas/Vaas.php b/php/src/vaas/Vaas.php index 4479c668..76291f90 100644 --- a/php/src/vaas/Vaas.php +++ b/php/src/vaas/Vaas.php @@ -106,7 +106,7 @@ public function ForSha256(string $hashString, string $uuid = null): VaasVerdict * @throws TimeoutException * @throws InvalidArgumentException */ - public function ForUrl(string $url, string $uuid = null): VaasVerdict + public function ForUrl(?string $url, string $uuid = null): VaasVerdict { if ($this->_logger != null) $this->_logger->debug("ForUrl", ["URL:" => $url]); @@ -169,28 +169,28 @@ public function ForFile(string $path, bool $upload = true, string $uuid = null): * @throws GuzzleException * @throws UploadFailedException */ - public function ForStream(Stream $stream, string $uuid=null): VaasVerdict + public function ForStream(Stream $stream, string $uuid = null): VaasVerdict { - if ($uuid == null){ + if ($uuid == null) { $uuid = UuidV4::getFactory()->uuid4()->toString(); } - + $verdictResponse = $this->_verdictResponseForStream($uuid); - - if ($verdictResponse->verdict != Verdict::UNKNOWN){ + + if ($verdictResponse->verdict != Verdict::UNKNOWN) { throw new VaasServerException("Server returned verdict without receiving content."); } - if ($verdictResponse->upload_token == null || $verdictResponse->upload_token == ""){ + if ($verdictResponse->upload_token == null || $verdictResponse->upload_token == "") { throw new JsonMapper_Exception("VerdictResponse missing UploadToken for stream upload."); } - if ($verdictResponse->url == null || $verdictResponse->url == ""){ + if ($verdictResponse->url == null || $verdictResponse->url == "") { throw new JsonMapper_Exception("VerdictResponse missing URL for stream upload."); } $this->UploadStream($stream, $verdictResponse->url, $verdictResponse->upload_token); $verdictResponse = $this->_waitForVerdict($uuid); - + return new VaasVerdict($verdictResponse); } @@ -335,7 +335,7 @@ private function _handleWebSocketErrorResponse(Error $errorResponse): void $details = null; } $errorType = $errorResponse->getType(); - if ($errorType == "ClientError"){ + if ($errorType == "ClientError") { throw new VaasClientException($details); } throw new VaasServerException($details); @@ -397,7 +397,7 @@ private function _verdictResponseForUrl(string $url, string $uuid = null): Verdi * @throws BadOpcodeException * @throws VaasInvalidStateException */ - private function _verdictResponseForStream(string $uuid=null): VerdictResponse + private function _verdictResponseForStream(string $uuid = null): VerdictResponse { if ($this->_logger != null) $this->_logger->debug("_verdictResponseForStream"); diff --git a/php/tests/vaas/VaasTest.php b/php/tests/vaas/VaasTest.php index d57dadc8..b7f3e5c1 100644 --- a/php/tests/vaas/VaasTest.php +++ b/php/tests/vaas/VaasTest.php @@ -406,7 +406,7 @@ public function testForUrl_WithInvalidUrl_ThrowsVaasClientException() public function testForUrl_WithNull_ThrowsVaasClientException() { $vaas = new Vaas($_ENV["VAAS_URL"], $this->_getDebugLogger()); - $this->expectException(\TypeError::class); + $this->expectException(\InvalidArgumentException::class); $vaas->Connect($this->getClientCredentialsGrantAuthenticator()->getToken()); $invalidUrl = null; @@ -449,7 +449,7 @@ public function testForStream_WithEicarString_ReturnsMalicious() $stream = fopen(sprintf('data://text/plain,%s', $eicar), 'r'); rewind($stream); $eicarStream = new Stream($stream); - + $verdict = $vaas->ForStream($eicarStream); $this->assertEquals(Verdict::MALICIOUS, $verdict->Verdict); @@ -500,9 +500,9 @@ public function testForStream_WithCleanUrlContentAsStream_ReturnsClean() $httpClient = new Client(); $response = $httpClient->get($url); $stream = new Stream($response->getBody()->detach()); - + $verdict = $vaas->ForStream($stream); - + $this->assertEquals(Verdict::CLEAN, $verdict->Verdict); } From 71c6396f2fdf3e3b935b71cf9b3a9a85dd6aad76 Mon Sep 17 00:00:00 2001 From: unglaublicherdude Date: Wed, 21 Feb 2024 11:54:48 +0100 Subject: [PATCH 6/6] add content-length to the upload --- php/src/vaas/Vaas.php | 1 + 1 file changed, 1 insertion(+) diff --git a/php/src/vaas/Vaas.php b/php/src/vaas/Vaas.php index 76291f90..bb00593e 100644 --- a/php/src/vaas/Vaas.php +++ b/php/src/vaas/Vaas.php @@ -462,6 +462,7 @@ private function UploadStream(Stream $stream, string $url, string $uploadToken) { $response = $this->_httpClient->put($url, [ 'body' => $stream, + 'content-length' => $stream->getSize(), 'timeout' => $this->_uploadTimeoutInSeconds, 'headers' => ["Authorization" => $uploadToken] ]);