Repository: google/tsunami-security-scanner Branch: master Commit: 32920de1868c Files: 345 Total size: 1.7 MB Directory structure: gitextract_306kc4re/ ├── .dockerignore ├── .gitattributes ├── .github/ │ └── workflows/ │ ├── core-build.yml │ ├── core-push.yml │ ├── devel-push.yml │ └── full-push.yml ├── .gitignore ├── LICENSE ├── README.md ├── build.gradle ├── common/ │ ├── README.md │ ├── build.gradle │ └── src/ │ ├── main/ │ │ └── java/ │ │ └── com/ │ │ └── google/ │ │ └── tsunami/ │ │ └── common/ │ │ ├── ErrorCode.java │ │ ├── TsunamiException.java │ │ ├── cli/ │ │ │ ├── CliOption.java │ │ │ └── CliOptionsModule.java │ │ ├── command/ │ │ │ ├── CommandExecutionThreadPool.java │ │ │ ├── CommandExecutor.java │ │ │ ├── CommandExecutorFactory.java │ │ │ └── CommandExecutorModule.java │ │ ├── concurrent/ │ │ │ ├── BaseThreadPoolModule.java │ │ │ ├── ScheduledThreadPoolModule.java │ │ │ └── ThreadPoolModule.java │ │ ├── config/ │ │ │ ├── ConfigException.java │ │ │ ├── ConfigLoader.java │ │ │ ├── ConfigModule.java │ │ │ ├── TsunamiConfig.java │ │ │ ├── YamlConfigLoader.java │ │ │ └── annotations/ │ │ │ └── ConfigProperties.java │ │ ├── data/ │ │ │ ├── NetworkEndpointUtils.java │ │ │ └── NetworkServiceUtils.java │ │ ├── io/ │ │ │ └── archiving/ │ │ │ ├── Archiver.java │ │ │ ├── GoogleCloudStorageArchiver.java │ │ │ ├── GoogleCloudStorageArchiverModule.java │ │ │ ├── RawFileArchiver.java │ │ │ └── testing/ │ │ │ ├── FakeArchiver.java │ │ │ ├── FakeGoogleCloudStorageArchivers.java │ │ │ ├── FakeGoogleCloudStorageArchiversModule.java │ │ │ ├── FakeRawFileArchiver.java │ │ │ └── FakeRawFileArchiverModule.java │ │ ├── net/ │ │ │ ├── FuzzingUtils.java │ │ │ ├── UrlUtils.java │ │ │ ├── db/ │ │ │ │ ├── ConnectionProvider.java │ │ │ │ └── ConnectionProviderInterface.java │ │ │ ├── http/ │ │ │ │ ├── HttpClient.java │ │ │ │ ├── HttpClientCliOptions.java │ │ │ │ ├── HttpClientConfigProperties.java │ │ │ │ ├── HttpClientModule.java │ │ │ │ ├── HttpHeaders.java │ │ │ │ ├── HttpMethod.java │ │ │ │ ├── HttpRequest.java │ │ │ │ ├── HttpResponse.java │ │ │ │ ├── HttpStatus.java │ │ │ │ ├── OkHttpHttpClient.java │ │ │ │ └── javanet/ │ │ │ │ ├── ConnectionFactory.java │ │ │ │ └── DefaultConnectionFactory.java │ │ │ └── socket/ │ │ │ ├── DefaultTsunamiSocketFactory.java │ │ │ ├── TsunamiSocketFactory.java │ │ │ ├── TsunamiSocketFactoryCliOptions.java │ │ │ ├── TsunamiSocketFactoryConfigProperties.java │ │ │ └── TsunamiSocketFactoryModule.java │ │ ├── reflection/ │ │ │ ├── ClassGraphModule.java │ │ │ └── RuntimeClassGraphScanResult.java │ │ ├── server/ │ │ │ ├── CompactRunRequestHelper.java │ │ │ └── LanguageServerCommand.java │ │ ├── time/ │ │ │ ├── SystemUtcClockModule.java │ │ │ ├── UtcClock.java │ │ │ └── testing/ │ │ │ ├── FakeUtcClock.java │ │ │ └── FakeUtcClockModule.java │ │ └── version/ │ │ ├── ComparisonUtility.java │ │ ├── KnownQualifier.java │ │ ├── Segment.java │ │ ├── Token.java │ │ ├── Version.java │ │ ├── VersionRange.java │ │ └── VersionSet.java │ └── test/ │ ├── java/ │ │ └── com/ │ │ └── google/ │ │ └── tsunami/ │ │ └── common/ │ │ ├── cli/ │ │ │ └── CliOptionsModuleTest.java │ │ ├── command/ │ │ │ ├── CommandExecutorFactoryTest.java │ │ │ └── CommandExecutorTest.java │ │ ├── concurrent/ │ │ │ ├── BaseThreadPoolModuleTest.java │ │ │ ├── ScheduledThreadPoolModuleTest.java │ │ │ └── ThreadPoolModuleTest.java │ │ ├── config/ │ │ │ ├── ConfigModuleTest.java │ │ │ ├── TsunamiConfigTest.java │ │ │ └── YamlConfigLoaderTest.java │ │ ├── data/ │ │ │ ├── NetworkEndpointUtilsTest.java │ │ │ └── NetworkServiceUtilsTest.java │ │ ├── io/ │ │ │ └── archiving/ │ │ │ ├── ArchiverTestUtils.java │ │ │ ├── GoogleCloudStorageArchiverTest.java │ │ │ └── RawFileArchiverTest.java │ │ ├── net/ │ │ │ ├── FuzzingUtilsTest.java │ │ │ ├── UrlUtilsTest.java │ │ │ ├── http/ │ │ │ │ ├── HttpClientModuleTest.java │ │ │ │ ├── HttpHeadersTest.java │ │ │ │ ├── HttpRequestTest.java │ │ │ │ ├── HttpResponseTest.java │ │ │ │ └── OkHttpHttpClientTest.java │ │ │ └── socket/ │ │ │ ├── DefaultTsunamiSocketFactoryTest.java │ │ │ ├── TsunamiSocketFactoryCliOptionsTest.java │ │ │ └── TsunamiSocketFactoryModuleTest.java │ │ ├── server/ │ │ │ └── CompactRunRequestHelperTest.java │ │ ├── time/ │ │ │ ├── SystemUtcClockModuleTest.java │ │ │ └── testing/ │ │ │ ├── FakeUtcClockModuleTest.java │ │ │ └── FakeUtcClockTest.java │ │ └── version/ │ │ ├── ComparisonUtilityTest.java │ │ ├── EqualsTestCase.java │ │ ├── KnownQualifierTest.java │ │ ├── LessThanTestCase.java │ │ ├── SegmentTest.java │ │ ├── TokenTest.java │ │ ├── VersionRangeTest.java │ │ ├── VersionSetTest.java │ │ └── VersionTest.java │ └── resources/ │ └── com/ │ └── google/ │ └── tsunami/ │ └── common/ │ └── net/ │ └── http/ │ └── testdata/ │ ├── README.md │ └── tsunami_test_server.p12 ├── core.Dockerfile ├── devel.Dockerfile ├── docs/ │ ├── _config.yml │ ├── _data/ │ │ └── nav.yml │ ├── _includes/ │ │ └── nav.html │ ├── _layouts/ │ │ ├── default.html │ │ ├── home.html │ │ └── post.html │ ├── _posts/ │ │ ├── 2024-03-19-tsunami-network-scanner-ai-security.md │ │ ├── 2025-06-18-changes-to-tsunami.md │ │ └── 2025-10-16-october-update-tsunami-prp.md │ ├── about/ │ │ └── index.md │ ├── assets/ │ │ └── css/ │ │ └── style.scss │ ├── blog/ │ │ └── index.html │ ├── contribute/ │ │ ├── code-of-conduct.md │ │ ├── contributing.md │ │ └── index.md │ ├── howto/ │ │ ├── common-patterns.md │ │ ├── howto.md │ │ ├── index.md │ │ ├── new-detector/ │ │ │ ├── new-detector-java.md │ │ │ └── templated/ │ │ │ ├── 00-getting-started.md │ │ │ ├── 01-introduction.md │ │ │ ├── 02-bootstrapping.md │ │ │ ├── 03-first-actions.md │ │ │ ├── 04-workflows.md │ │ │ ├── 05-variables.md │ │ │ ├── 06-callback-server.md │ │ │ ├── 07-cleanup-actions.md │ │ │ ├── 08-writing-unit-tests.md │ │ │ ├── appendix-naming-actions.md │ │ │ ├── appendix-naming-plugin.md │ │ │ ├── appendix-naming-tests.md │ │ │ ├── appendix-using-linter.md │ │ │ ├── glossary-predefined-variables.md │ │ │ └── glossary-tests-magic-uri.md │ │ └── orchestration.md │ └── index.md ├── full.Dockerfile ├── go.mod ├── main/ │ ├── README.md │ ├── build.gradle │ └── src/ │ ├── main/ │ │ └── java/ │ │ └── com/ │ │ └── google/ │ │ └── tsunami/ │ │ └── main/ │ │ └── cli/ │ │ ├── LanguageServerOptions.java │ │ ├── ScanResultsArchiver.java │ │ ├── ScanResultsArchiverModule.java │ │ ├── TsunamiCli.java │ │ ├── option/ │ │ │ ├── MainCliOptions.java │ │ │ ├── OutputDataFormat.java │ │ │ └── validator/ │ │ │ ├── IpV4Validator.java │ │ │ ├── IpV6Validator.java │ │ │ └── IpValidator.java │ │ └── server/ │ │ ├── RemoteServerLoader.java │ │ └── RemoteServerLoaderModule.java │ └── test/ │ └── java/ │ └── com/ │ └── google/ │ └── tsunami/ │ └── main/ │ └── cli/ │ ├── LanguageServerOptionsTest.java │ ├── ScanResultsArchiverTest.java │ ├── TsunamiCliTest.java │ ├── option/ │ │ ├── MainCliOptionsTest.java │ │ ├── OutputDataFormatTest.java │ │ └── validator/ │ │ ├── IpV4ValidatorTest.java │ │ ├── IpV6ValidatorTest.java │ │ └── IpValidatorTest.java │ └── server/ │ └── RemoteServerLoaderTest.java ├── plugin/ │ ├── README.md │ ├── build.gradle │ └── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── google/ │ │ │ └── tsunami/ │ │ │ └── plugin/ │ │ │ ├── LanguageServerException.java │ │ │ ├── PluginBootstrapModule.java │ │ │ ├── PluginDefinition.java │ │ │ ├── PluginExecutionException.java │ │ │ ├── PluginExecutionModule.java │ │ │ ├── PluginExecutionResult.java │ │ │ ├── PluginExecutionThreadPool.java │ │ │ ├── PluginExecutor.java │ │ │ ├── PluginExecutorImpl.java │ │ │ ├── PluginExecutorModule.java │ │ │ ├── PluginLoadingModule.java │ │ │ ├── PluginManager.java │ │ │ ├── PluginManagerCliOptions.java │ │ │ ├── PluginServiceClient.java │ │ │ ├── PluginType.java │ │ │ ├── PortScanner.java │ │ │ ├── RemoteVulnDetector.java │ │ │ ├── RemoteVulnDetectorImpl.java │ │ │ ├── RemoteVulnDetectorLoadingModule.java │ │ │ ├── ServiceFingerprinter.java │ │ │ ├── TcsClient.java │ │ │ ├── TcsClientCliOptions.java │ │ │ ├── TcsConfigProperties.java │ │ │ ├── TsunamiPlugin.java │ │ │ ├── VulnDetector.java │ │ │ ├── annotations/ │ │ │ │ ├── ForOperatingSystemClass.java │ │ │ │ ├── ForServiceName.java │ │ │ │ ├── ForSoftware.java │ │ │ │ ├── ForWebService.java │ │ │ │ ├── PluginInfo.java │ │ │ │ └── RequiresCallbackServer.java │ │ │ ├── payload/ │ │ │ │ ├── NotImplementedException.java │ │ │ │ ├── Payload.java │ │ │ │ ├── PayloadGenerator.java │ │ │ │ ├── PayloadGeneratorModule.java │ │ │ │ ├── PayloadSecretGenerator.java │ │ │ │ ├── README.md │ │ │ │ ├── Validator.java │ │ │ │ └── testing/ │ │ │ │ ├── FakePayloadGeneratorModule.java │ │ │ │ └── PayloadTestHelper.java │ │ │ └── testing/ │ │ │ ├── FailedPortScanner.java │ │ │ ├── FailedPortScannerBootstrapModule.java │ │ │ ├── FailedRemoteVulnDetector.java │ │ │ ├── FailedRemoteVulnDetectorBootstrapModule.java │ │ │ ├── FailedServiceFingerprinter.java │ │ │ ├── FailedServiceFingerprinterBootstrapModule.java │ │ │ ├── FailedVulnDetector.java │ │ │ ├── FailedVulnDetectorBootstrapModule.java │ │ │ ├── FakePluginExecutionModule.java │ │ │ ├── FakePortScanner.java │ │ │ ├── FakePortScanner2.java │ │ │ ├── FakePortScannerBootstrapModule.java │ │ │ ├── FakePortScannerBootstrapModule2.java │ │ │ ├── FakePortScannerBootstrapModuleEmpty.java │ │ │ ├── FakePortScannerEmpty.java │ │ │ ├── FakeRemoteVulnDetector.java │ │ │ ├── FakeRemoteVulnDetectorBootstrapModule.java │ │ │ ├── FakeServiceFingerprinter.java │ │ │ ├── FakeServiceFingerprinterBootstrapModule.java │ │ │ ├── FakeVulnDetector.java │ │ │ ├── FakeVulnDetector2.java │ │ │ ├── FakeVulnDetectorBootstrapModule.java │ │ │ ├── FakeVulnDetectorBootstrapModule2.java │ │ │ ├── FakeVulnDetectorBootstrapModuleEmpty.java │ │ │ └── FakeVulnDetectorEmpty.java │ │ └── resources/ │ │ └── com/ │ │ └── google/ │ │ └── tsunami/ │ │ └── plugin/ │ │ └── payload/ │ │ └── payload_definitions.yaml │ └── test/ │ └── java/ │ └── com/ │ └── google/ │ └── tsunami/ │ └── plugin/ │ ├── PluginDefinitionTest.java │ ├── PluginExecutorImplTest.java │ ├── PluginLoadingModuleTest.java │ ├── PluginManagerTest.java │ ├── PluginServiceClientTest.java │ ├── RemoteVulnDetectorImplTest.java │ ├── RemoteVulnDetectorLoadingModuleTest.java │ ├── TcsClientTest.java │ └── payload/ │ ├── PayloadGeneratorModuleTest.java │ ├── PayloadGeneratorWithCallbackServerTest.java │ ├── PayloadGeneratorWithoutCallbackServerTest.java │ ├── PayloadSecretGeneratorTest.java │ └── PayloadTest.java ├── plugin_server/ │ └── py/ │ ├── common/ │ │ ├── data/ │ │ │ ├── network_endpoint_utils.py │ │ │ ├── network_endpoint_utils_test.py │ │ │ ├── network_service_utils.py │ │ │ └── network_service_utils_test.py │ │ └── net/ │ │ └── http/ │ │ ├── host_resolver_http_adapter.py │ │ ├── host_resolver_http_adapter_test.py │ │ ├── http_client.py │ │ ├── http_header_fields.py │ │ ├── http_header_fields_test.py │ │ ├── http_headers.py │ │ ├── http_headers_test.py │ │ ├── http_method.py │ │ ├── http_request.py │ │ ├── http_request_test.py │ │ ├── http_response.py │ │ ├── http_response_test.py │ │ ├── http_status.py │ │ ├── http_status_test.py │ │ ├── requests_http_client.py │ │ └── requests_http_client_test.py │ ├── plugin/ │ │ ├── payload/ │ │ │ ├── payload.py │ │ │ ├── payload_generator.py │ │ │ ├── payload_generator_test.py │ │ │ ├── payload_generator_test_helper.py │ │ │ ├── payload_secret_generator.py │ │ │ ├── payload_secret_generator_test.py │ │ │ ├── payload_test.py │ │ │ ├── payload_utility.py │ │ │ ├── payload_utility_test.py │ │ │ └── validator.py │ │ ├── tcs_client.py │ │ └── tcs_client_test.py │ ├── plugin_server.py │ ├── plugin_service.py │ ├── plugin_service_test.py │ ├── requirements.in │ ├── requirements.txt │ └── tsunami_plugin.py ├── proto/ │ ├── build.gradle │ ├── detection.proto │ ├── go/ │ │ ├── detection_go_proto/ │ │ │ └── detection.pb.go │ │ ├── network_go_proto/ │ │ │ └── network.pb.go │ │ ├── network_service_go_proto/ │ │ │ └── network_service.pb.go │ │ ├── payload_generator_go_proto/ │ │ │ └── payload_generator.pb.go │ │ ├── plugin_representation_go_proto/ │ │ │ └── plugin_representation.pb.go │ │ ├── plugin_service_go_proto/ │ │ │ └── plugin_service.pb.go │ │ ├── reconnaissance_go_proto/ │ │ │ └── reconnaissance.pb.go │ │ ├── scan_results_go_proto/ │ │ │ └── scan_results.pb.go │ │ ├── scan_target_go_proto/ │ │ │ └── scan_target.pb.go │ │ ├── software_go_proto/ │ │ │ └── software.pb.go │ │ ├── vulnerability_go_proto/ │ │ │ └── vulnerability.pb.go │ │ └── web_crawl_go_proto/ │ │ └── web_crawl.pb.go │ ├── network.proto │ ├── network_service.proto │ ├── payload_generator.proto │ ├── plugin_representation.proto │ ├── plugin_service.proto │ ├── reconnaissance.proto │ ├── scan_results.proto │ ├── scan_target.proto │ ├── software.proto │ ├── tsunami_go_proto/ │ │ ├── detection.pb.go │ │ ├── network.pb.go │ │ ├── network_service.pb.go │ │ ├── payload_generator.pb.go │ │ ├── plugin_representation.pb.go │ │ ├── plugin_service.pb.go │ │ ├── reconnaissance.pb.go │ │ ├── scan_results.pb.go │ │ ├── scan_target.pb.go │ │ ├── software.pb.go │ │ ├── vulnerability.pb.go │ │ └── web_crawl.pb.go │ ├── vulnerability.proto │ └── web_crawl.proto ├── settings.gradle ├── tsunami.yaml ├── tsunami_tcs.yaml └── workflow/ ├── README.md ├── build.gradle └── src/ ├── main/ │ └── java/ │ └── com/ │ └── google/ │ └── tsunami/ │ └── workflow/ │ ├── AdvisoriesWorkflow.java │ ├── DefaultScanningWorkflow.java │ ├── ExecutionStage.java │ ├── ExecutionTracer.java │ └── ScanningWorkflowException.java └── test/ └── java/ └── com/ └── google/ └── tsunami/ └── workflow/ ├── DefaultScanningWorkflowTest.java └── ExecutionTracerTest.java ================================================ FILE CONTENTS ================================================ ================================================ FILE: .dockerignore ================================================ # Git-related files .gitattributes .gitignore .git/ # IDE files .idea/ .vscode/ # Build cache .gradle/ # Documentation docs/ LICENSE README.md # Miscellaneous quick_start.sh Dockerfile .dockerignore ================================================ FILE: .gitattributes ================================================ # # https://help.github.com/articles/dealing-with-line-endings/ # # These are explicitly windows files and should use crlf *.bat text eol=crlf ================================================ FILE: .github/workflows/core-build.yml ================================================ name: core-build on: pull_request: workflow_dispatch: env: REGISTRY: ghcr.io IMAGE_NAME: google/tsunami-scanner-core jobs: build-image: runs-on: ubuntu-latest permissions: contents: read steps: - name: Checkout repository uses: actions/checkout@v4 - name: Build Docker image id: build uses: docker/build-push-action@v6 with: context: . file: core.Dockerfile push: false tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} labels: ${{ steps.meta.outputs.labels }} ================================================ FILE: .github/workflows/core-push.yml ================================================ name: core-push on: push: branches: - master workflow_dispatch: env: REGISTRY: ghcr.io IMAGE_NAME: google/tsunami-scanner-core jobs: build-and-push-image: runs-on: ubuntu-latest permissions: contents: read packages: write attestations: write id-token: write steps: - name: Checkout repository uses: actions/checkout@v4 - name: Log in to the Container registry uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Extract metadata for Docker id: meta uses: docker/metadata-action@v5 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - name: Build and push Docker image id: push uses: docker/build-push-action@v6 with: context: . file: core.Dockerfile push: true tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} labels: ${{ steps.meta.outputs.labels }} - name: Generate artifact attestation uses: actions/attest-build-provenance@v2 with: subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} subject-digest: ${{ steps.push.outputs.digest }} push-to-registry: true ================================================ FILE: .github/workflows/devel-push.yml ================================================ name: devel-push on: schedule: - cron: "0 */4 * * *" workflow_dispatch: env: REGISTRY: ghcr.io IMAGE_NAME: google/tsunami-scanner-devel jobs: build-and-push-image: runs-on: ubuntu-latest permissions: contents: read packages: write attestations: write id-token: write steps: - name: Checkout repository uses: actions/checkout@v4 - name: Log in to the Container registry uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Extract metadata for Docker id: meta uses: docker/metadata-action@v5 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - name: Build and push Docker image id: push uses: docker/build-push-action@v6 with: context: . file: devel.Dockerfile push: true tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} labels: ${{ steps.meta.outputs.labels }} - name: Generate artifact attestation uses: actions/attest-build-provenance@v2 with: subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} subject-digest: ${{ steps.push.outputs.digest }} push-to-registry: true ================================================ FILE: .github/workflows/full-push.yml ================================================ name: full-push on: schedule: - cron: "0 */4 * * *" workflow_dispatch: env: REGISTRY: ghcr.io IMAGE_NAME: google/tsunami-scanner-full jobs: build-and-push-image: runs-on: ubuntu-latest permissions: contents: read packages: write attestations: write id-token: write steps: - name: Checkout repository uses: actions/checkout@v4 - name: Log in to the Container registry uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Extract metadata for Docker id: meta uses: docker/metadata-action@v5 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - name: Build and push Docker image id: push uses: docker/build-push-action@v6 with: context: . push: true file: full.Dockerfile tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} labels: ${{ steps.meta.outputs.labels }} - name: Generate artifact attestation uses: actions/attest-build-provenance@v2 with: subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} subject-digest: ${{ steps.push.outputs.digest }} push-to-registry: true ================================================ FILE: .gitignore ================================================ # Gradle build gradle.properties .gradle local.properties out # IntelliJ IDEA .idea *.iml *.ipr *.iws classes # Eclipse .classpath .factorypath .project .settings bin eclipsebin # OS X .DS_Store # Emacs *~ \#*\# ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================ # Tsunami ![build](https://github.com/google/tsunami-security-scanner/actions/workflows/core-build.yml/badge.svg) Tsunami is a general purpose network security scanner with an extensible plugin system for detecting high severity vulnerabilities with high confidence. To learn more about Tsunami, visit our [documentation](https://google.github.io/tsunami-security-scanner/). Tsunami relies heavily on its plugin system to provide basic scanning capabilities. All publicly available Tsunami plugins are hosted in a separate [google/tsunami-security-scanner-plugins](https://github.com/google/tsunami-security-scanner-plugins) repository. ## Quick start Please see the documentation on how to [build and run Tsunami](https://google.github.io/tsunami-security-scanner/howto/howto) ## Contributing Read how to [contribute to Tsunami](https://google.github.io/tsunami-security-scanner/contribute/). ## License Tsunami is released under the [Apache 2.0 license](LICENSE). ``` Copyright 2025 Google Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ``` ## Disclaimers Tsunami is not an official Google product. ================================================ FILE: build.gradle ================================================ // Current gradle version 6.5. plugins { id 'net.ltgt.errorprone' apply false version "4.2.0" id "com.gradleup.shadow" version "8.3.6" } subprojects { apply plugin: 'java' apply plugin: 'maven-publish' apply plugin: 'idea' apply plugin: 'net.ltgt.errorprone' apply plugin: 'com.gradleup.shadow' group = 'com.google.tsunami' version = '0.1.1-SNAPSHOT' // Current Tsunami version repositories { maven { // The google mirror is less flaky than mavenCentral() url 'https://maven-central.storage-download.googleapis.com/repos/central/data/' } mavenCentral() mavenLocal() } if (rootProject.properties.get('errorProne', true)) { dependencies { errorprone "com.google.errorprone:error_prone_core:2.38.0" errorproneJavac 'com.google.errorprone:javac:9+181-r4173-1' } // Disable ErrorProne for all generated codes. tasks.withType(JavaCompile).configureEach { options.errorprone.disableWarningsInGeneratedCode = false options.errorprone.excludedPaths = '.*/build/generated/.*' } } else { // Disable Error Prone allprojects { afterEvaluate { project -> project.tasks.withType(JavaCompile) { options.errorprone.enabled = false } } } } plugins.withId('java') { sourceCompatibility = JavaVersion.VERSION_21 targetCompatibility = JavaVersion.VERSION_21 java.withJavadocJar() java.withSourcesJar() jar.manifest { attributes('Implementation-Title': name, 'Implementation-Version': version, 'Built-By': System.getProperty('user.name'), 'Built-JDK': System.getProperty('java.version'), 'Source-Compatibility': sourceCompatibility, 'Target-Compatibility': targetCompatibility) } // Log stacktrace to console when test fails. test { testLogging { exceptionFormat = 'full' showExceptions true showCauses true showStackTraces true } maxHeapSize = '1500m' } } plugins.withId('maven-publish') { shadowJar { archiveClassifier = null } } } ================================================ FILE: common/README.md ================================================ # Tsunami Common Libraries ## Overview This module provides a set of common libraries and utilities for Tsunami Security Scanner. ================================================ FILE: common/build.gradle ================================================ description = 'Tsunami: Common' dependencies { implementation project(':tsunami-proto') implementation "com.beust:jcommander:1.48" implementation "com.google.auto.value:auto-value-annotations:1.11.0" implementation "com.google.cloud:google-cloud-storage:1.103.1" implementation "com.google.code.gson:gson:2.10.1" implementation "com.google.flogger:flogger-system-backend:0.9" implementation "com.google.flogger:flogger:0.9" implementation "com.google.flogger:google-extensions:0.9" implementation "com.google.guava:guava:33.0.0-jre" implementation "com.google.inject:guice:6.0.0" implementation "com.google.inject.extensions:guice-assistedinject:6.0.0" implementation "com.google.truth:truth:1.4.4" implementation "com.squareup.okhttp3:okhttp:3.12.0" implementation "io.github.classgraph:classgraph:4.8.65" implementation "org.yaml:snakeyaml:1.26" runtimeOnly "com.mysql:mysql-connector-j:8.0.33" runtimeOnly "org.apache.hive:hive-jdbc:4.0.1" runtimeOnly "org.postgresql:postgresql:42.6.0" annotationProcessor "com.google.auto.value:auto-value:1.10.4" testAnnotationProcessor "com.google.auto.value:auto-value:1.10.4" testImplementation "com.google.guava:guava-testlib:33.0.0-jre" testImplementation "com.google.truth:truth:1.4.4" testImplementation "com.google.truth.extensions:truth-java8-extension:1.4.4" testImplementation "com.google.truth.extensions:truth-proto-extension:1.4.4" testImplementation "com.squareup.okhttp3:mockwebserver:3.12.0" testImplementation "junit:junit:4.13.2" testImplementation "org.mockito:mockito-core:5.18.0" } ================================================ FILE: common/src/main/java/com/google/tsunami/common/ErrorCode.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common; /** Error codes for Tsunami scanner executions. */ public enum ErrorCode { CONFIG_ERROR, PLUGIN_EXECUTION_ERROR, WORKFLOW_ERROR, LANGUAGE_SERVER_ERROR, UNKNOWN; } ================================================ FILE: common/src/main/java/com/google/tsunami/common/TsunamiException.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common; import static com.google.common.base.Preconditions.checkNotNull; import com.google.common.base.Strings; /** Base exception definition of all Tsunami execution errors. */ public class TsunamiException extends RuntimeException { private final ErrorCode errorCode; public TsunamiException() { this(ErrorCode.UNKNOWN); } public TsunamiException(ErrorCode errorCode) { this(errorCode, null); } public TsunamiException(ErrorCode errorCode, String message) { this(errorCode, message, null); } public TsunamiException(ErrorCode errorCode, String message, Throwable cause) { super(buildExceptionMessage(checkNotNull(errorCode), message), cause); this.errorCode = errorCode; } private static String buildExceptionMessage(ErrorCode errorCode, String message) { StringBuilder exceptionMessageBuilder = new StringBuilder(); exceptionMessageBuilder.append("(Tsunami error ").append(errorCode).append(")"); if (!Strings.isNullOrEmpty(message)) { exceptionMessageBuilder.append(": ").append(message); } return exceptionMessageBuilder.toString(); } public ErrorCode getErrorCode() { return errorCode; } } ================================================ FILE: common/src/main/java/com/google/tsunami/common/cli/CliOption.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.cli; /** * A marker interface for a subset of command line options used in Tsunami modules. * *

Client should ALWAYS mark its options with this interface so that they can be identified by * {@link io.github.classgraph.ClassGraph}. All implementations of {@link CliOption} should provide * a no argument constructor or omit constructors completely. */ public interface CliOption { /** * Performs additional validation logic across options defined in the same {@link CliOption}. * *

If validation failed, simply throw a {@link com.beust.jcommander.ParameterException}. */ void validate(); } ================================================ FILE: common/src/main/java/com/google/tsunami/common/cli/CliOptionsModule.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.cli; import static com.google.common.base.Preconditions.checkNotNull; import com.beust.jcommander.JCommander; import com.beust.jcommander.ParameterException; import com.google.common.collect.ImmutableList; import com.google.common.flogger.GoogleLogger; import com.google.inject.AbstractModule; import io.github.classgraph.ClassInfo; import io.github.classgraph.ScanResult; import java.lang.reflect.Constructor; /** * A Guice module that parses CLI arguments for all {@link CliOption} implementations at runtime. * *

This module relies on the {@link io.github.classgraph.ClassGraph} scan results to identify all * {@link CliOption} implementations at runtime. Each implementation is bound to a singleton object * of that impl and registered to JCommander for CLI parsing. */ public final class CliOptionsModule extends AbstractModule { private static final GoogleLogger logger = GoogleLogger.forEnclosingClass(); private static final String CLI_OPTION_INTERFACE = "com.google.tsunami.common.cli.CliOption"; private final ScanResult scanResult; private final String[] args; private final JCommander jCommander; public CliOptionsModule(ScanResult scanResult, String programName, String[] args) { this.scanResult = checkNotNull(scanResult); this.args = checkNotNull(args); this.jCommander = new JCommander(); jCommander.setProgramName(programName); } @Override protected void configure() { // For each CliOption installed at runtime, bind a singleton instance and register the instance // to JCommander for parsing. ImmutableList.Builder cliOptions = ImmutableList.builder(); for (ClassInfo classInfo : scanResult .getClassesImplementing(CLI_OPTION_INTERFACE) .filter(classInfo -> !classInfo.isInterface())) { logger.atInfo().log("Found CliOption: %s", classInfo.getName()); CliOption cliOption = bindCliOption(classInfo.loadClass(CliOption.class)); jCommander.addObject(cliOption); cliOptions.add(cliOption); } // Parse command arguments or die. try { jCommander.parse(args); cliOptions.build().forEach(CliOption::validate); } catch (ParameterException e) { jCommander.usage(); throw e; } } private T bindCliOption(Class cliOptionClass) { try { Constructor cliOptionCtor = cliOptionClass.getDeclaredConstructor(); // Always create an instance of the CliOption regardless of scope. cliOptionCtor.setAccessible(true); T cliOption = cliOptionCtor.newInstance(); bind(cliOptionClass).toInstance(cliOption); return cliOption; } catch (ReflectiveOperationException e) { throw new AssertionError( String.format( "CliOption '%s' must be constructable via a no-argument constructor", cliOptionClass.getTypeName()), e); } } } ================================================ FILE: common/src/main/java/com/google/tsunami/common/command/CommandExecutionThreadPool.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.command; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import javax.inject.Qualifier; /** Annotates the thread pool to use for executing native commands. */ @Qualifier @Retention(RetentionPolicy.RUNTIME) public @interface CommandExecutionThreadPool {} ================================================ FILE: common/src/main/java/com/google/tsunami/common/command/CommandExecutor.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.command; import static com.google.common.base.Preconditions.checkNotNull; import static java.nio.charset.StandardCharsets.UTF_8; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Joiner; import com.google.common.flogger.GoogleLogger; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.ThreadPoolExecutor; import javax.annotation.Nullable; /** Helper class that handles running a command line and collecting output and errors. */ // TODO(b/145315535): reimplement this class so that it is: // 1. guice injectable in order to hide Executor interface. // 2. unit testable to prevent actually executing commands in test. public class CommandExecutor { private static final GoogleLogger logger = GoogleLogger.forEnclosingClass(); private static final Joiner COMMAND_ARGS_JOINER = Joiner.on(" "); private final ProcessBuilder processBuilder; private final String[] args; private Process process; @Nullable private String output; @Nullable private String error; public CommandExecutor(String... args) { this.args = checkNotNull(args); this.processBuilder = new ProcessBuilder(args); } /* * Executes the command and uses a {@link ThreadPoolExecutor} to collect output and error. * * This is a convenience method for testing purposes only as the executor is not shared and * therefore defeats the purpose of having a cached thread pool. */ @VisibleForTesting Process execute() throws IOException, InterruptedException, ExecutionException { // Nmap is a long running process and the collectStream method is a blocking method. // By default CompletableFuture uses ForkJoinPool, which is for suitable short // non-blocking operations. Executor executor = Executors.newCachedThreadPool(); return execute(executor); } /** * Starts the command and uses the passed executor to collect output and error streams. * *

IMPORTANT: The stream collection uses an IO blocking method and the passed executor must be * well suited for the task. {@link ThreadPoolExecutor} is a viable option. * * @param executor The executor to collect output and error streams. * @return Started {@link Process} object. * @throws IOException if an I/O error occurs when starting the command executing process. * @throws InterruptedException if interrupted while waiting for the command's output. * @throws ExecutionException if the command execution failed. */ public Process execute(Executor executor) throws IOException, InterruptedException, ExecutionException { logger.atInfo().log("Executing the following command: '%s'", COMMAND_ARGS_JOINER.join(args)); process = processBuilder.start(); output = CompletableFuture.supplyAsync(() -> collectStream(process.getInputStream()), executor) .get(); error = CompletableFuture.supplyAsync(() -> collectStream(process.getErrorStream()), executor) .get(); return process; } /** * Starts the command and asynchronously collect output and error. * * @return Started {@link Process} object. * @throws IOException if an I/O error occurs when starting the command executing process. * @throws InterruptedException if interrupted while waiting for the command's output. * @throws ExecutionException if the command execution failed. */ public Process executeAsync() throws IOException, InterruptedException, ExecutionException { logger.atInfo().log("Executing the following command: '%s'", COMMAND_ARGS_JOINER.join(args)); process = processBuilder.inheritIO().start(); return process; } /** * Starts the command without collecting output and error streams. * * @return Started {@link Process} object. * @throws IOException if an I/O error occurs when starting the command executing process. * @throws InterruptedException if interrupted while starting the command executing process. * @throws ExecutionException if the command execution failed. */ public Process executeWithNoStreamCollection() throws IOException, InterruptedException, ExecutionException { logger.atInfo().log("Executing the following command: '%s'", COMMAND_ARGS_JOINER.join(args)); process = processBuilder.start(); return process; } @Nullable public String getOutput() { return output; } @Nullable public String getError() { return error; } private static String collectStream(InputStream stream) { StringBuilder stringBuilder = new StringBuilder(); try { String output; BufferedReader streamReader = new BufferedReader(new InputStreamReader(stream, UTF_8)); while ((output = streamReader.readLine()) != null) { stringBuilder.append(output); stringBuilder.append("\n"); } } catch (IOException e) { logger.atWarning().withCause(e).log("Error collecting output stream from command execution."); } return stringBuilder.toString(); } } ================================================ FILE: common/src/main/java/com/google/tsunami/common/command/CommandExecutorFactory.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.command; /** Utility class to simplify the creation and testing of {@link CommandExecutor} instances. */ public class CommandExecutorFactory { private static CommandExecutor instance; /** * Sets an executor instance that will be returned by all future calls to {@link * CommandExecutorFactory#create(String...)} * * @param executor The {@link CommandExecutor} returned by this factory. */ public static void setInstance(CommandExecutor executor) { instance = executor; } /** * Creates a new {@link CommandExecutor} if none is set. * * @param args List of arguments to pass to the newly created {@link CommandExecutor}. * @return the {@link CommandExecutor} instance created by this factory. */ public static CommandExecutor create(String... args) { if (instance == null) { return new CommandExecutor(args); } return instance; } private CommandExecutorFactory() {} } ================================================ FILE: common/src/main/java/com/google/tsunami/common/command/CommandExecutorModule.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.command; import com.google.inject.AbstractModule; import com.google.tsunami.common.concurrent.ThreadPoolModule; /** Installs dependencies used by {@link CommandExecutor}. */ public class CommandExecutorModule extends AbstractModule { @Override protected void configure() { install( new ThreadPoolModule.Builder() .setName("CommandExecutor") .setCoreSize(4) .setMaxSize(8) .setQueueCapacity(32) .setDaemon(true) .setPriority(Thread.NORM_PRIORITY) .setAnnotation(CommandExecutionThreadPool.class) .build()); } } ================================================ FILE: common/src/main/java/com/google/tsunami/common/concurrent/BaseThreadPoolModule.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.concurrent; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; import com.google.common.base.Strings; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.ThreadFactoryBuilder; import com.google.inject.AbstractModule; import com.google.inject.Key; import java.lang.annotation.Annotation; import java.time.Duration; import java.util.concurrent.ExecutorService; import java.util.concurrent.RejectedExecutionHandler; import java.util.concurrent.ThreadFactory; import java.util.concurrent.ThreadPoolExecutor.AbortPolicy; import java.util.concurrent.TimeUnit; import javax.inject.Provider; import org.checkerframework.checker.nullness.qual.Nullable; /** * The base module for binding a thread pool. * *

This module is essentially a thin wrapper around {@link ThreadFactoryBuilder} and the * corresponding {@link ExecutorService} families. Based on the intended usage, it is expected that * subclasses of this module should provides bindings to a concrete thread pool implementation of * {@link ExecutorService}. This base module wraps the actual {@link ExecutorService} implementation * in order to support {@link com.google.common.util.concurrent.ListenableFuture} usage in the code * base. * * @param The expected thread pool implementation, must be a subclass of {@link * ListeningExecutorService}. */ abstract class BaseThreadPoolModule extends AbstractModule { private final ThreadFactory factory; private final int maxSize; private final int coreSize; private final long keepAliveSeconds; private final @Nullable Duration shutdownDelay; private final Key key; private final Class executorServiceTypeClass; private final RejectedExecutionHandler rejectedExecutionHandler; BaseThreadPoolModule(BaseThreadPoolModuleBuilder builder) { checkNotNull(builder); this.factory = builder.factoryBuilder.build(); this.maxSize = builder.maxSize; this.coreSize = builder.coreSize; this.keepAliveSeconds = builder.keepAliveSeconds; this.shutdownDelay = builder.daemon ? builder.shutdownDelay : null; this.key = builder.key; this.executorServiceTypeClass = builder.executorServiceTypeClass; this.rejectedExecutionHandler = builder.rejectedExecutionHandler; } @Override protected final void configure() { configureThreadPool(key); } /** Subclasses should override this method for providing Guice bindings. */ abstract void configureThreadPool(Key key); /** Base {@link Provider} implementation for providing the target thread pool. */ abstract class BaseThreadPoolProvider implements Provider { abstract ExecutorService createThreadPool( int coreSize, int maxSize, long keepAliveSeconds, ThreadFactory factory, RejectedExecutionHandler rejectedExecutionHandler); @Override public final ExecutorServiceT get() { ExecutorService service = createThreadPool(coreSize, maxSize, keepAliveSeconds, factory, rejectedExecutionHandler); if (shutdownDelay != null) { MoreExecutors.addDelayedShutdownHook( service, shutdownDelay.toMillis(), TimeUnit.MILLISECONDS); } return executorServiceTypeClass.cast(MoreExecutors.listeningDecorator(service)); } } /** Base Builder for {@link BaseThreadPoolModule}. */ abstract static class BaseThreadPoolModuleBuilder< ExecutorServiceT extends ListeningExecutorService, BuilderImplT extends BaseThreadPoolModuleBuilder> { protected final ThreadFactoryBuilder factoryBuilder = new ThreadFactoryBuilder(); protected String name; protected int maxSize; protected int coreSize; protected long keepAliveSeconds = 60L; protected boolean daemon; protected Duration shutdownDelay; protected Key key; protected final Class executorServiceTypeClass; protected RejectedExecutionHandler rejectedExecutionHandler = new AbortPolicy(); BaseThreadPoolModuleBuilder(Class executorServiceTypeClass) { this.executorServiceTypeClass = checkNotNull(executorServiceTypeClass); } abstract BuilderImplT self(); /** * Sets the name used to name the threads; automatically suffixed with "-%s"to incorporate the * thread number * * @param name the name of the thread pool. * @return the Builder instance itself. */ public BuilderImplT setName(String name) { checkArgument(!Strings.isNullOrEmpty(name), "Name should not be empty"); this.name = name; return self(); } /** * Sets the maximum number of threads allowed in the pool; value should be positive. * * @param maxSize the maximum number of threads allowed in this thread pool. * @return the Builder instance itself. */ BuilderImplT setMaxSize(int maxSize) { checkArgument(maxSize > 0, "Max thread pool size should be positive."); this.maxSize = maxSize; return self(); } /** * Sets the number of threads to keep in the pool. * * @param coreSize the minimum number of threads to keep alive in this thread pool. * @return the Builder instance itself. */ BuilderImplT setCoreSize(int coreSize) { checkArgument(coreSize >= 0, "The core pool size should be non-negative."); this.coreSize = coreSize; return self(); } /** * Sets the keep alive time in seconds for the threads not in core pool. * * @param keepAliveSeconds the maximum number of seconds an idle thread in this pool can keep * alive before being terminated. * @return the Builder instance itself. */ public BuilderImplT setKeepAliveSeconds(long keepAliveSeconds) { checkArgument(keepAliveSeconds >= 0, "The keep alive time should be non-negative."); this.keepAliveSeconds = keepAliveSeconds; return self(); } /** * Sets whether or not new threads created by the pool will be daemon threads.* * * @param daemon whether threads created in this pool are daemon threads. * @return the Builder instance itself. */ public BuilderImplT setDaemon(boolean daemon) { factoryBuilder.setDaemon(daemon); this.daemon = daemon; return self(); } /** * Sets how long the JVM should wait to exit for daemon threads to complete. * *

This has no effect if the pool does not use daemon threads. * * @param shutdownDelay the delay enforced during the thread pool shutdown. * @return the Builder instance itself. */ public BuilderImplT setDelayedShutdown(Duration shutdownDelay) { this.shutdownDelay = checkNotNull(shutdownDelay); return self(); } /** * Sets the priority for threads created by the pool. * * @param priority the priority of the threads created by this pool. * @return the Builder instance itself. */ public BuilderImplT setPriority(int priority) { factoryBuilder.setPriority(priority); return self(); } /** * Sets the binding annotation. * * @param annotation the Guice binding annotation for this thread pool. * @return the Builder instance itself. */ public BuilderImplT setAnnotation(Annotation annotation) { key = Key.get(executorServiceTypeClass, checkNotNull(annotation)); return self(); } /** * Sets the binding annotation. * * @param annotationClass the Guice binding annotation class for this thread pool. * @return the Builder instance itself. */ public BuilderImplT setAnnotation(Class annotationClass) { key = Key.get(executorServiceTypeClass, checkNotNull(annotationClass)); return self(); } /** * Sets the handler to use when thread execution is blocked due to thread bounds and queue * capacities are reached. * *

By default, {@link AbortPolicy} is used for rejected execution, which throws the {@link * java.util.concurrent.RejectedExecutionException}. * * @param rejectedExecutionHandler A handler for tasks that cannot be executed by this thread * pool. * @return the Builder instance itself. */ public BuilderImplT setRejectedExecutionHandler( RejectedExecutionHandler rejectedExecutionHandler) { this.rejectedExecutionHandler = checkNotNull(rejectedExecutionHandler); return self(); } final void validateAll() { checkState(!Strings.isNullOrEmpty(name), "Name is required."); checkState( maxSize > 0, "Max thread pool size must be positive. Did you forget setting maximum thread pool size" + " by calling setMaxSize?"); checkState( coreSize <= maxSize, "Thread pool core size should be less than or equal to max size."); checkState(key != null, "Annotation is required."); validate(); } abstract void validate(); public final AbstractModule build() { validateAll(); return newModule(); } abstract AbstractModule newModule(); } } ================================================ FILE: common/src/main/java/com/google/tsunami/common/concurrent/ScheduledThreadPoolModule.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.concurrent; import static com.google.common.base.Preconditions.checkArgument; import static java.util.concurrent.TimeUnit.SECONDS; import com.google.common.util.concurrent.ListeningScheduledExecutorService; import com.google.inject.Key; import java.util.concurrent.RejectedExecutionHandler; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.ThreadFactory; import javax.inject.Singleton; /** * A helper module for binding a scheduled thread pool. The module will bind a {@link * ScheduledExecutorService} and a {@link ListeningScheduledExecutorService} to a singleton thread * pool that is annotated with the annotation passed to the builder. */ public final class ScheduledThreadPoolModule extends BaseThreadPoolModule { ScheduledThreadPoolModule(Builder builder) { super(builder); } @Override void configureThreadPool(Key key) { bind(key.ofType(ScheduledExecutorService.class)).to(key); bind(key).toProvider(new ScheduledThreadPoolProvider()).in(Singleton.class); } private final class ScheduledThreadPoolProvider extends BaseThreadPoolProvider { @Override ScheduledThreadPoolExecutor createThreadPool( int coreSize, int maxSize, long keepAliveSeconds, ThreadFactory factory, RejectedExecutionHandler rejectedExecutionHandler) { ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(coreSize, factory, rejectedExecutionHandler); scheduledThreadPoolExecutor.setMaximumPoolSize(maxSize); scheduledThreadPoolExecutor.setKeepAliveTime(keepAliveSeconds, SECONDS); return scheduledThreadPoolExecutor; } } /** * Builder for {@link ScheduledThreadPoolModule}. * *

NOTE: Unlike {@link ThreadPoolModule}, {@link ScheduledThreadPoolExecutor} acts as a * fixed-sized pool using {@code corePoolSize} threads and an unbounded queue. So this builder * only allows users to set a fixed thread pool size. */ public static final class Builder extends BaseThreadPoolModuleBuilder { public Builder() { super(ListeningScheduledExecutorService.class); } @Override Builder self() { return this; } /** * Sets the size of the thread pool. * * @param size the size of the thread pool. * @return the {@link Builder} instance itself. */ public Builder setSize(int size) { checkArgument(size > 0, "Thread pool size should be positive."); setCoreSize(size); setMaxSize(size); return this; } @Override void validate() {} @Override ScheduledThreadPoolModule newModule() { return new ScheduledThreadPoolModule(this); } } } ================================================ FILE: common/src/main/java/com/google/tsunami/common/concurrent/ThreadPoolModule.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.concurrent; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.inject.Key; import com.google.inject.Singleton; import java.util.concurrent.BlockingQueue; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.RejectedExecutionHandler; import java.util.concurrent.SynchronousQueue; import java.util.concurrent.ThreadFactory; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import org.checkerframework.checker.nullness.qual.Nullable; /** * A helper module for binding a thread pool. The module will bind an {@link Executor}, {@link * ExecutorService} and {@link ListeningExecutorService} annotated with the annotation passed to the * builder. */ public final class ThreadPoolModule extends BaseThreadPoolModule { private final BlockingQueue blockingQueue; private ThreadPoolModule(Builder builder) { super(checkNotNull(builder)); this.blockingQueue = builder.getBlockingQueue(); } @Override void configureThreadPool(Key key) { bind(key.ofType(Executor.class)).to(key); bind(key.ofType(ExecutorService.class)).to(key); bind(key).toProvider(new ThreadPoolProvider()).in(Singleton.class); } private final class ThreadPoolProvider extends BaseThreadPoolProvider { @Override ThreadPoolExecutor createThreadPool( int coreSize, int maxSize, long keepAliveSeconds, ThreadFactory factory, RejectedExecutionHandler rejectedExecutionHandler) { return new ThreadPoolExecutor( coreSize, maxSize, keepAliveSeconds, TimeUnit.SECONDS, blockingQueue, factory, rejectedExecutionHandler); } } /** Builder for {@link ThreadPoolModule}. */ public static final class Builder extends BaseThreadPoolModuleBuilder { private int queueCapacity = Integer.MAX_VALUE; private @Nullable BlockingQueue blockingQueue; public Builder() { super(ListeningExecutorService.class); } @Override Builder self() { return this; } /** {@inheritDoc} */ @Override public Builder setMaxSize(int maxSize) { return super.setMaxSize(maxSize); } /** {@inheritDoc} */ @Override public Builder setCoreSize(int coreSize) { return super.setCoreSize(coreSize); } /** * Sets the queue capacity for the thread pool. * *

NOTE: Users should NOT specify both this value and the {@link BlockingQueue} via {@link * #setBlockingQueue}. * *

By default, {@link SynchronousQueue} will be used when {@code queueCapacity} is set to * zero. Otherwise a {@link LinkedBlockingQueue} will be used. * * @param queueCapacity the capacity of the task queue. * @return the Builder instance itself. */ public Builder setQueueCapacity(int queueCapacity) { checkArgument(queueCapacity >= 0, "The queue capacity should be non-negative value."); this.queueCapacity = queueCapacity; return this; } /** * Sets the {@link BlockingQueue} to use for holding tasks before they are executed. * *

NOTE: Do NOT set both {@link BlockingQueue} and {@code queueCapacity}. Only use this * method to override the default {@link BlockingQueue} choice. See comments of {@link * #getBlockingQueue} for which {@link BlockingQueue} is used by default. * * @param blockingQueue a {@link BlockingQueue} used for holding tasks before executing. * @return the Builder instance itself. */ public Builder setBlockingQueue(BlockingQueue blockingQueue) { this.blockingQueue = checkNotNull(blockingQueue); return this; } private BlockingQueue getBlockingQueue() { if (blockingQueue == null) { return queueCapacity == 0 ? new SynchronousQueue<>() : new LinkedBlockingQueue<>(queueCapacity); } return blockingQueue; } private boolean isBoundedQueue() { return (blockingQueue == null ? queueCapacity : blockingQueue.remainingCapacity()) < Integer.MAX_VALUE; } @Override void validate() { checkState( blockingQueue == null || queueCapacity == Integer.MAX_VALUE, "Both custom BlockingQueue and queue capacity are specified."); if (coreSize < maxSize) { checkState( isBoundedQueue(), "Finite capacity queue should be set when the core pool size is less than max pool" + " size. ThreadPoolExecutor will only create new threads past core size when the" + " queue is full."); } } @Override ThreadPoolModule newModule() { return new ThreadPoolModule(this); } } } ================================================ FILE: common/src/main/java/com/google/tsunami/common/config/ConfigException.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.config; import com.google.tsunami.common.ErrorCode; import com.google.tsunami.common.TsunamiException; /** Exception when handling Tsunami configs. */ public class ConfigException extends TsunamiException { public ConfigException(String message) { super(ErrorCode.CONFIG_ERROR, message); } public ConfigException(String message, Throwable cause) { super(ErrorCode.CONFIG_ERROR, message, cause); } } ================================================ FILE: common/src/main/java/com/google/tsunami/common/config/ConfigLoader.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.config; /** Config loader interface that load Tsunami configs from certain data sources. */ public interface ConfigLoader { /** * Load a {@link TsunamiConfig} object from certain data source. * * @return the loaded {@link TsunamiConfig} object. */ TsunamiConfig loadConfig(); } ================================================ FILE: common/src/main/java/com/google/tsunami/common/config/ConfigModule.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.config; import static com.google.common.base.Preconditions.checkNotNull; import com.google.common.flogger.GoogleLogger; import com.google.inject.AbstractModule; import io.github.classgraph.ClassInfo; import io.github.classgraph.ScanResult; /** * A Guice module that binds all Tsunami config objects at runtime. * *

This module relies on the {@link io.github.classgraph.ClassGraph} scan results to identify all * Tsunami config objects annotated by the {@link * com.google.tsunami.common.config.annotations.ConfigProperties} annotation. Each config class is * bound to a singleton object whose fields are populated from the Tsunami config file. */ public final class ConfigModule extends AbstractModule { private static final GoogleLogger logger = GoogleLogger.forEnclosingClass(); private static final String CONFIG_PROPERTIES_ANNOTATION = "com.google.tsunami.common.config.annotations.ConfigProperties"; private final ScanResult scanResult; private final TsunamiConfig tsunamiConfig; public ConfigModule(ScanResult scanResult, TsunamiConfig tsunamiConfig) { this.scanResult = checkNotNull(scanResult); this.tsunamiConfig = checkNotNull(tsunamiConfig); } @Override protected void configure() { bind(TsunamiConfig.class).toInstance(tsunamiConfig); for (ClassInfo configClass : scanResult .getClassesWithAnnotation(CONFIG_PROPERTIES_ANNOTATION) .filter(classInfo -> !classInfo.isAbstract())) { logger.atInfo().log("Found Tsunami config class: %s", configClass.getName()); bindConfigClass(getConfigPrefix(configClass), configClass.loadClass()); } } private void bindConfigClass(String configPrefix, Class configClass) { T configObject = tsunamiConfig.getConfig(configPrefix, configClass); bind(configClass).toInstance(configObject); } private static String getConfigPrefix(ClassInfo configClass) { Object configPrefix = configClass .getAnnotationInfo(CONFIG_PROPERTIES_ANNOTATION) .getParameterValues() .getValue("value"); if (!(configPrefix instanceof String)) { throw new AssertionError("SHOULD NEVER HAPPEN, ConfigProperties value is not a string."); } return (String) configPrefix; } } ================================================ FILE: common/src/main/java/com/google/tsunami/common/config/TsunamiConfig.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.config; import static com.google.common.base.Preconditions.checkNotNull; import com.google.common.base.CaseFormat; import com.google.common.base.Converter; import com.google.common.base.Splitter; import com.google.common.collect.ImmutableMap; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.util.Map; import java.util.Optional; /** A data holder for all Tsunami config data, including config files and Java system properties. */ public final class TsunamiConfig { private static final Converter FIELD_NAME_TO_LOWER_UNDERSCORE = CaseFormat.LOWER_CAMEL.converterTo(CaseFormat.LOWER_UNDERSCORE); private static final Splitter CONFIG_PATH_SPLITTER = Splitter.on('.').omitEmptyStrings(); private final ImmutableMap rawConfigData; private TsunamiConfig(ImmutableMap rawConfigData) { this.rawConfigData = checkNotNull(rawConfigData); } public ImmutableMap getRawConfigData() { return rawConfigData; } public static TsunamiConfig fromYamlData(Map yamlConfig) { return new TsunamiConfig( yamlConfig == null ? ImmutableMap.of() : ImmutableMap.copyOf(yamlConfig)); } public static Optional getSystemProperty(String propertyName) { return Optional.ofNullable(getSystemProperty(propertyName, null)); } public static String getSystemProperty(String propertyName, String def) { return System.getProperty(propertyName, def); } /** * Get a config object with the given {@code configPrefix} and bind all config values to the * requested {@code clazz}. * *

This code uses reflection to create the requested config object. The request type {@code T} * must provide a no-argument or default constructor. * * @param configPrefix the prefix of the config to be read from. * @param clazz the class of the returned config object. * @param actual config object type. * @return an object whose field values are filled by the config data under the given {@code * configPrefix}. */ public T getConfig(String configPrefix, Class clazz) { checkNotNull(configPrefix); checkNotNull(clazz); Map configValue = readConfigValue(configPrefix); return newConfigObject(clazz, configValue); } @SuppressWarnings("unchecked") // We know Map key is always String from yaml file. public ImmutableMap readConfigValue(String configPrefix) { Map retrievedData = rawConfigData; // Config prefixes are dot separated words list, e.g. example.config.prefix. for (String configKey : CONFIG_PATH_SPLITTER.split(configPrefix)) { // Requested data not found under configPrefix. if (!retrievedData.containsKey(configKey)) { return ImmutableMap.of(); } Object configData = retrievedData.get(configKey); if (!(configData instanceof Map)) { throw new ConfigException( String.format( "Unexpected data type for config '%s', expected '%s', got '%s'", configKey, Map.class, configData.getClass())); } retrievedData = (Map) configData; } return ImmutableMap.copyOf(retrievedData); } private static T newConfigObject(Class clazz, Map configValue) { try { Constructor configObjectCtor = clazz.getDeclaredConstructor(); // Always create an instance of the config data regardless of scope. configObjectCtor.setAccessible(true); T configObject = configObjectCtor.newInstance(); // Fill each field of the configObject from configValue using the field name as key. for (Field field : clazz.getDeclaredFields()) { String fieldName = field.getName(); if (configValue.containsKey(fieldName) || configValue.containsKey(FIELD_NAME_TO_LOWER_UNDERSCORE.convert(fieldName))) { Object fieldValue = Optional.ofNullable(configValue.get(fieldName)) .orElse(configValue.get(FIELD_NAME_TO_LOWER_UNDERSCORE.convert(fieldName))); field.setAccessible(true); field.set(configObject, fieldValue); } } return configObject; } catch (ReflectiveOperationException e) { // This is bad. Config objects cannot be created or config value cannot be assigned to the // field, we throw assertion error and fail the execution. throw new AssertionError( String.format( "Unable to create new instance of '%s' using config value '%s'", clazz, configValue), e); } } } ================================================ FILE: common/src/main/java/com/google/tsunami/common/config/YamlConfigLoader.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.config; import static java.nio.charset.StandardCharsets.UTF_8; import com.google.common.flogger.GoogleLogger; import com.google.common.io.Files; import java.io.File; import java.io.FileNotFoundException; import java.io.Reader; import java.io.StringReader; import java.util.Map; import org.yaml.snakeyaml.LoaderOptions; import org.yaml.snakeyaml.Yaml; import org.yaml.snakeyaml.constructor.SafeConstructor; /** A {@link ConfigLoader} implementation that loads Tsunami configs from YAML file. */ public final class YamlConfigLoader implements ConfigLoader { private static final GoogleLogger logger = GoogleLogger.forEnclosingClass(); private static final String DEFAULT_CONFIG_FILE = "tsunami.yaml"; @Override public TsunamiConfig loadConfig() { Yaml yaml = new Yaml(new SafeConstructor(new LoaderOptions())); Map rawYamlData = yaml.load(configFileReader()); return TsunamiConfig.fromYamlData(rawYamlData); } private static Reader configFileReader() { String configFile = TsunamiConfig.getSystemProperty("tsunami.config.location").orElse(DEFAULT_CONFIG_FILE); try { return Files.newReader(new File(configFile), UTF_8); } catch (FileNotFoundException e) { logger.atWarning().log( "Unable to read config file '%s', default to empty config.", configFile); return new StringReader(""); } } } ================================================ FILE: common/src/main/java/com/google/tsunami/common/config/annotations/ConfigProperties.java ================================================ /* * Copyright 2019 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.config.annotations; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * An annotation for marking a Tsunami config object that can be initialized from external config * files, e.g. a {@code .yaml} file. * *

This annotation is required for any config object in order for Tsunami initialization logic to * identify and automatically populate config properties. * * Example usage: * *

{@code
 * {@literal @}ConfigProperties("example.config.location")})
 * public class ExampleConfig {
 *   // ...
 * }
 * }
*/ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface ConfigProperties { /** * The required prefix of the properties that should be bound to the annotated object. * *

A valid prefix is defined as dot separated words list (e.g. "plugin.example.abc"). Each dot * separated segment represents a section within the config file. For example, given a YAML file * *

{@code
   * plugin:
   *   example:
   *     abc:
   *       fieldA: valueA
   *       fieldB: valueB
   *     xyz:
   *       fieldC: valueC
   * }
* * value {@code "plugin.example.abc"} will select {@code fieldA} and {@code fieldB} for config * binding for the annotated class. * * @return the prefix of the config properties. */ String value(); } ================================================ FILE: common/src/main/java/com/google/tsunami/common/data/NetworkEndpointUtils.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * For any utility update, please consider if Python's network endpoint utils * (plugin_server/py/common/data/network_endpoint_utils.py) also needs the modification. */ package com.google.tsunami.common.data; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import com.google.common.net.HostAndPort; import com.google.common.net.InetAddresses; import com.google.tsunami.proto.AddressFamily; import com.google.tsunami.proto.Hostname; import com.google.tsunami.proto.IpAddress; import com.google.tsunami.proto.NetworkEndpoint; import com.google.tsunami.proto.Port; import java.net.Inet4Address; import java.net.Inet6Address; import java.net.InetAddress; /** Static utility methods pertaining to {@link NetworkEndpoint} proto buffer. */ public final class NetworkEndpointUtils { public static final int MAX_PORT_NUMBER = 65535; private NetworkEndpointUtils() {} public static boolean hasIpAddress(NetworkEndpoint networkEndpoint) { return networkEndpoint.getType().equals(NetworkEndpoint.Type.IP) || networkEndpoint.getType().equals(NetworkEndpoint.Type.IP_PORT) || networkEndpoint.getType().equals(NetworkEndpoint.Type.IP_HOSTNAME) || networkEndpoint.getType().equals(NetworkEndpoint.Type.IP_HOSTNAME_PORT); } public static boolean hasHostname(NetworkEndpoint networkEndpoint) { return networkEndpoint.getType().equals(NetworkEndpoint.Type.HOSTNAME) || networkEndpoint.getType().equals(NetworkEndpoint.Type.HOSTNAME_PORT) || networkEndpoint.getType().equals(NetworkEndpoint.Type.IP_HOSTNAME) || networkEndpoint.getType().equals(NetworkEndpoint.Type.IP_HOSTNAME_PORT); } public static boolean hasPort(NetworkEndpoint networkEndpoint) { return networkEndpoint.getType().equals(NetworkEndpoint.Type.IP_PORT) || networkEndpoint.getType().equals(NetworkEndpoint.Type.HOSTNAME_PORT) || networkEndpoint.getType().equals(NetworkEndpoint.Type.IP_HOSTNAME_PORT); } public static boolean isIpV6Endpoint(NetworkEndpoint networkEndpoint) { return hasIpAddress(networkEndpoint) && networkEndpoint.getIpAddress().getAddressFamily().equals(AddressFamily.IPV6); } /** * Converts the given {@link NetworkEndpoint} to its uri authority representation. * *

For example: * *

* * @param networkEndpoint the {@link NetworkEndpoint} instance to be converted. * @return the URI authority converted from the {@link NetworkEndpoint} instance. */ public static String toUriAuthority(NetworkEndpoint networkEndpoint) { return toHostAndPort(networkEndpoint).toString(); } public static HostAndPort toHostAndPort(NetworkEndpoint networkEndpoint) { switch (networkEndpoint.getType()) { case IP: return HostAndPort.fromHost(networkEndpoint.getIpAddress().getAddress()); case IP_PORT: return HostAndPort.fromParts( networkEndpoint.getIpAddress().getAddress(), networkEndpoint.getPort().getPortNumber()); case HOSTNAME: case IP_HOSTNAME: return HostAndPort.fromHost(networkEndpoint.getHostname().getName()); case HOSTNAME_PORT: case IP_HOSTNAME_PORT: return HostAndPort.fromParts( networkEndpoint.getHostname().getName(), networkEndpoint.getPort().getPortNumber()); case UNRECOGNIZED: case TYPE_UNSPECIFIED: throw new AssertionError("Type for NetworkEndpoint must be specified."); } throw new AssertionError( String.format( "Should never happen. Unchecked NetworkEndpoint type: %s", networkEndpoint.getType())); } /** * Creates a {@link NetworkEndpoint} proto buffer object from the given ip address. * * @param ipAddress the IP address of the network endpoint. * @return the created {@link NetworkEndpoint} instance from the given IP address. */ public static NetworkEndpoint forIp(String ipAddress) { checkArgument(InetAddresses.isInetAddress(ipAddress), "'%s' is not an IP address.", ipAddress); return NetworkEndpoint.newBuilder() .setType(NetworkEndpoint.Type.IP) .setIpAddress( IpAddress.newBuilder() .setAddressFamily(ipAddressFamily(ipAddress)) .setAddress(ipAddress)) .build(); } /** * Creates a {@link NetworkEndpoint} proto buffer object from the given ip address and port. * * @param ipAddress the IP address of the network endpoint. * @param port the port number of the network endpoint * @return the created {@link NetworkEndpoint} instance from the given IP and port. */ public static NetworkEndpoint forIpAndPort(String ipAddress, int port) { checkArgument(InetAddresses.isInetAddress(ipAddress), "'%s' is not an IP address.", ipAddress); checkArgument( 0 <= port && port <= MAX_PORT_NUMBER, "Port out of range. Expected [0, %s], actual %s.", MAX_PORT_NUMBER, port); return forIp(ipAddress).toBuilder() .setType(NetworkEndpoint.Type.IP_PORT) .setPort(Port.newBuilder().setPortNumber(port)) .build(); } /** * Creates a {@link NetworkEndpoint} proto buffer object from the given hostname. * * @param hostname the hostname of the network endpoint * @return the created {@link NetworkEndpoint} instance from the hostname. */ public static NetworkEndpoint forHostname(String hostname) { checkArgument( !InetAddresses.isInetAddress(hostname), "Expected hostname, got IP address '%s'", hostname); return NetworkEndpoint.newBuilder() .setType(NetworkEndpoint.Type.HOSTNAME) .setHostname(Hostname.newBuilder().setName(hostname)) .build(); } /** * Creates a {@link NetworkEndpoint} proto buffer object from the given ip address and hostname. * * @param hostname the hostname of the network endpoint * @param ipAddress the IP address of the network endpoint. * @return the created {@link NetworkEndpoint} instance from the IP address and hostname. */ public static NetworkEndpoint forIpAndHostname(String ipAddress, String hostname) { return forIp(ipAddress).toBuilder() .setType(NetworkEndpoint.Type.IP_HOSTNAME) .setHostname(Hostname.newBuilder().setName(hostname)) .build(); } /** * Creates a {@link NetworkEndpoint} proto buffer object from the given hostname and port. * * @param hostname the hostname of the network endpoint * @param port the port number of the network endpoint. * @return the created {@link NetworkEndpoint} instance from the hostname and port. */ public static NetworkEndpoint forHostnameAndPort(String hostname, int port) { checkArgument( 0 <= port && port <= MAX_PORT_NUMBER, "Port out of range. Expected [0, %s], actual %s.", MAX_PORT_NUMBER, port); return forHostname(hostname).toBuilder() .setType(NetworkEndpoint.Type.HOSTNAME_PORT) .setPort(Port.newBuilder().setPortNumber(port)) .build(); } /** * Returns a {@link NetworkEndpoint} proto buffer object from the given ip address, hostname and * port. * * @param ipAddress the IP address of the network endpoint. * @param hostname the hostname of the network endpoint * @param port the port number of the network endpoint. * @return the created {@link NetworkEndpoint} instance from the parameters. */ public static NetworkEndpoint forIpHostnameAndPort(String ipAddress, String hostname, int port) { checkArgument( 0 <= port && port <= MAX_PORT_NUMBER, "Port out of range. Expected [0, %s], actual %s.", MAX_PORT_NUMBER, port); return forIpAndHostname(ipAddress, hostname).toBuilder() .setType(NetworkEndpoint.Type.IP_HOSTNAME_PORT) .setPort(Port.newBuilder().setPortNumber(port)) .build(); } /** * Returns a {@link NetworkEndpoint} proto buffer object from the given {@code networkEndpoint} * and port. The {@code networkEndpoint} parameter cannot contain any port information, otherwise * {@link IllegalArgumentException} is thrown. * * @param networkEndpoint the source {@link NetworkEndpoint} instance without the port number * @param port the port number of the network endpoint. * @return the {@link NetworkEndpoint} instance from the parameters. */ public static NetworkEndpoint forNetworkEndpointAndPort( NetworkEndpoint networkEndpoint, int port) { checkNotNull(networkEndpoint); checkArgument( 0 <= port && port <= MAX_PORT_NUMBER, "Port out of range. Expected [0, %s], actual %s.", MAX_PORT_NUMBER, port); switch (networkEndpoint.getType()) { case IP: return networkEndpoint.toBuilder() .setType(NetworkEndpoint.Type.IP_PORT) .setPort(Port.newBuilder().setPortNumber(port)) .build(); case HOSTNAME: return networkEndpoint.toBuilder() .setType(NetworkEndpoint.Type.HOSTNAME_PORT) .setPort(Port.newBuilder().setPortNumber(port)) .build(); case IP_HOSTNAME: return networkEndpoint.toBuilder() .setType(NetworkEndpoint.Type.IP_HOSTNAME_PORT) .setPort(Port.newBuilder().setPortNumber(port)) .build(); case IP_PORT: case HOSTNAME_PORT: case IP_HOSTNAME_PORT: case UNRECOGNIZED: case TYPE_UNSPECIFIED: throw new IllegalArgumentException("Invalid NetworkEndpoint type."); } throw new AssertionError( String.format( "Should never happen. Unchecked NetworkEndpoint type: %s", networkEndpoint.getType())); } public static AddressFamily ipAddressFamily(String ipAddress) { InetAddress inetAddress = InetAddresses.forString(ipAddress); if (inetAddress instanceof Inet4Address) { return AddressFamily.IPV4; } else if (inetAddress instanceof Inet6Address) { return AddressFamily.IPV6; } else { throw new AssertionError(String.format("Unknown IP address family for IP '%s'", ipAddress)); } } } ================================================ FILE: common/src/main/java/com/google/tsunami/common/data/NetworkServiceUtils.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * For any utility update, please consider if Python's network service utils * (plugin_server/py/common/data/network_service_utils.py) also needs the modification. */ package com.google.tsunami.common.data; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import com.google.common.base.Ascii; import com.google.common.collect.ImmutableMap; import com.google.tsunami.proto.AddressFamily; import com.google.tsunami.proto.Hostname; import com.google.tsunami.proto.IpAddress; import com.google.tsunami.proto.NetworkEndpoint; import com.google.tsunami.proto.NetworkService; import com.google.tsunami.proto.Port; import com.google.tsunami.proto.ServiceContext; import com.google.tsunami.proto.TransportProtocol; import com.google.tsunami.proto.WebServiceContext; import java.net.Inet4Address; import java.net.Inet6Address; import java.net.InetAddress; import java.net.URI; import java.net.URISyntaxException; import java.net.UnknownHostException; import java.util.Optional; /** Static utility methods pertaining to {@link NetworkService} proto buffer. */ public final class NetworkServiceUtils { // Service names are those described in [RFC6335]. private static final ImmutableMap IS_PLAIN_HTTP_BY_KNOWN_WEB_SERVICE_NAME = ImmutableMap.builder() .put("http", true) .put("http-alt", true) // Some http server are identified as this rather than "http". .put("http-proxy", true) .put("https", false) .put("radan-http", true) // Port 8088, Hadoop Yarn web UI identified as this. .put("ssl/http", false) .put("ssl/https", false) .put("ssl/http-proxy", false) .put("ssl/tungsten-https", false) // Port 9443, WSO2 Identity Server & WSO2 API Manager. .put("ssl/wso2esb-console", false) // Port 9444, WSO2 Identity Server Analytics. .build(); private NetworkServiceUtils() {} public static boolean isWebService(Optional serviceName) { return serviceName.isPresent() && IS_PLAIN_HTTP_BY_KNOWN_WEB_SERVICE_NAME.containsKey( Ascii.toLowerCase(serviceName.get())); } public static boolean isWebService(NetworkService networkService) { checkNotNull(networkService); // A web-service is a service that is either flagged as http by nmap or one that supports at // least one HTTP method. return (networkService.getSupportedHttpMethodsCount() > 0) || isWebService(Optional.of(networkService.getServiceName())); } public static boolean isPlainHttp(NetworkService networkService) { checkNotNull(networkService); var isWebService = isWebService(networkService); var isKnownServiceName = IS_PLAIN_HTTP_BY_KNOWN_WEB_SERVICE_NAME.containsKey( Ascii.toLowerCase(networkService.getServiceName())); var doesNotSupportAnySslVersion = networkService.getSupportedSslVersionsCount() == 0; if (!isKnownServiceName) { return isWebService && doesNotSupportAnySslVersion; } var isKnownPlainHttpService = IS_PLAIN_HTTP_BY_KNOWN_WEB_SERVICE_NAME.getOrDefault( Ascii.toLowerCase(networkService.getServiceName()), false); return isKnownPlainHttpService && doesNotSupportAnySslVersion; } public static String getServiceName(NetworkService networkService) { if (isWebService(networkService) && networkService.hasSoftware()) { return Ascii.toLowerCase(networkService.getSoftware().getName()); } return Ascii.toLowerCase(networkService.getServiceName()); } public static String getWebServiceName(NetworkService networkService) { if (isWebService(networkService) && networkService.getServiceContext().getWebServiceContext().hasSoftware()) { return Ascii.toLowerCase( networkService.getServiceContext().getWebServiceContext().getSoftware().getName()); } return Ascii.toLowerCase(networkService.getServiceName()); } public static NetworkService buildUriNetworkService(String uriString) { try { URI uri = new URI(uriString); NetworkEndpoint uriEndPoint = buildUriNetworkEndPoint(uri); return NetworkService.newBuilder() .setNetworkEndpoint(uriEndPoint) .setTransportProtocol(TransportProtocol.TCP) .setServiceName(uri.getScheme()) .setServiceContext( ServiceContext.newBuilder() .setWebServiceContext( WebServiceContext.newBuilder().setApplicationRoot(uri.getPath()))) .build(); } catch (URISyntaxException exception) { throw new AssertionError( String.format( "Invalid uri syntax passed as target '%s'. Error: %s", uriString, exception)); } } private static NetworkEndpoint buildUriNetworkEndPoint(URI uri) { try { String hostname = uri.getHost(); String scheme = uri.getScheme(); checkArgument( scheme.equals("http") || scheme.equals("https"), "Uri scheme should be one of the following: 'http', 'https'"); int port = uri.getPort(); if (port < 0) { port = scheme.equals("http") ? 80 : 443; } String ipAddress = InetAddress.getByName(hostname).getHostAddress(); InetAddress inetAddress = InetAddress.getByName(uri.getHost()); checkArgument( (inetAddress instanceof Inet4Address) || (inetAddress instanceof Inet6Address), "Invalid address family"); AddressFamily addressFamily = inetAddress instanceof Inet4Address ? AddressFamily.IPV4 : AddressFamily.IPV6; return NetworkEndpoint.newBuilder() .setType(NetworkEndpoint.Type.IP_HOSTNAME_PORT) .setPort(Port.newBuilder().setPortNumber(port)) .setHostname(Hostname.newBuilder().setName(uri.getHost())) .setIpAddress( IpAddress.newBuilder().setAddressFamily(addressFamily).setAddress(ipAddress)) .build(); } catch (UnknownHostException exception) { throw new AssertionError( String.format("Unable to get valid host from uri. Error: %s", exception)); } } /** * Build the root url for a web application service. * * @param networkService a web (http/https) service * @return the root url for the web service, which always ends with a "/". */ public static String buildWebApplicationRootUrl(NetworkService networkService) { checkNotNull(networkService); if (!isWebService(networkService)) { return "http://" + NetworkEndpointUtils.toUriAuthority(networkService.getNetworkEndpoint()) + "/"; } String rootUrl = (isPlainHttp(networkService) ? "http://" : "https://") + buildWebUriAuthority(networkService) + buildWebAppRootPath(networkService); return rootUrl.endsWith("/") ? rootUrl : rootUrl + "/"; } private static String buildWebAppRootPath(NetworkService networkService) { String rootPath = networkService.getServiceContext().hasWebServiceContext() ? networkService.getServiceContext().getWebServiceContext().getApplicationRoot() : "/"; if (!rootPath.startsWith("/")) { rootPath = "/" + rootPath; } return rootPath; } private static String buildWebUriAuthority(NetworkService networkService) { String uriAuthority = NetworkEndpointUtils.toUriAuthority(networkService.getNetworkEndpoint()); // Remove default ports of the protocol. boolean isPlainHttp = isPlainHttp(networkService); if (isPlainHttp && uriAuthority.endsWith(":80")) { uriAuthority = uriAuthority.substring(0, uriAuthority.lastIndexOf(":80")); } if (!isPlainHttp && uriAuthority.endsWith(":443")) { uriAuthority = uriAuthority.substring(0, uriAuthority.lastIndexOf(":443")); } return uriAuthority; } } ================================================ FILE: common/src/main/java/com/google/tsunami/common/io/archiving/Archiver.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.io.archiving; import static com.google.common.base.Preconditions.checkNotNull; import static java.nio.charset.StandardCharsets.UTF_8; import com.google.errorprone.annotations.CanIgnoreReturnValue; /** An {@link Archiver} archives the given data to some data storage. */ public interface Archiver { /** * Archives the {@code data} associated with the given {@code name}. * * @param name the name that will be associated with the data * @param data the data to be archived in byte array format * @return whether the given data is archived successfully. */ @CanIgnoreReturnValue boolean archive(String name, byte[] data); /** * Archives the {@code data} associated with the given {@code name}. By default, this method * encodes the {@link CharSequence} {@code data} into a sequence of bytes using {@code UTF_8} * {@link java.nio.charset.StandardCharsets} and calls the {@link #archive(String, byte[])} * method. * * @param name the name that will be associated with the data * @param data the data to be archived in {@link CharSequence} format * @return whether the given data is archived successfully. */ @CanIgnoreReturnValue default boolean archive(String name, CharSequence data) { return archive(name, checkNotNull(data).toString().getBytes(UTF_8)); } } ================================================ FILE: common/src/main/java/com/google/tsunami/common/io/archiving/GoogleCloudStorageArchiver.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.io.archiving; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import com.beust.jcommander.Parameter; import com.beust.jcommander.Parameters; import com.beust.jcommander.validators.PositiveInteger; import com.google.cloud.WriteChannel; import com.google.cloud.storage.BlobInfo; import com.google.cloud.storage.Storage; import com.google.common.flogger.GoogleLogger; import com.google.inject.assistedinject.Assisted; import com.google.tsunami.common.cli.CliOption; import java.io.IOException; import java.nio.ByteBuffer; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.inject.Inject; /** An {@link Archiver} implementation that archives data into Google Cloud Storage. */ public class GoogleCloudStorageArchiver implements Archiver { private static final GoogleLogger logger = GoogleLogger.forEnclosingClass(); // For sanity-checking and to parse out the bucket name and object id. // See https://cloud.google.com/storage/docs/bucket-naming public static final Pattern GS_URL_PATTERN = Pattern.compile("gs://([^/]{3,63})/(.*)"); private final Options options; private final Storage storage; /** All command line options for {@link GoogleCloudStorageArchiver}. */ @Parameters(separators = "=") public static final class Options implements CliOption { @Parameter( names = "--gcs-archiver-chunk-size-in-bytes", description = "The size of the data chunk when GCS archiver uploads data to Cloud Storage.", validateWith = PositiveInteger.class) int chunkSizeInBytes = 1_000; @Parameter( names = "--gcs-archiver-chunk-upload-threshold-in-bytes", description = "The default data size threshold in bytes to enable chunk upload to GCS.", validateWith = PositiveInteger.class) int chunkUploadThresholdInBytes = 1_000_000; @Override public void validate() {} } @Inject public GoogleCloudStorageArchiver(Options options, @Assisted Storage storage) { this.options = checkNotNull(options); this.storage = checkNotNull(storage); } private static BlobInfo parseBlobInfo(String gcsUrl) { Matcher matcher = GS_URL_PATTERN.matcher(gcsUrl); checkArgument(matcher.matches(), "Invalid GCS URL: '%s'", gcsUrl); String bucketName = matcher.group(1); String objectName = matcher.group(2); return BlobInfo.newBuilder(bucketName, objectName).build(); } @Override public boolean archive(String gcsUrl, byte[] data) { BlobInfo blobInfo = parseBlobInfo(gcsUrl); if (data.length <= options.chunkUploadThresholdInBytes) { // Create the blob in one request. logger.atInfo().log("Archiving data to GCS at '%s' in one request.", gcsUrl); storage.create(blobInfo, data); return true; } // When content is large (1MB or more) it is recommended to write it in chunks via the blob's // channel writer. logger.atInfo().log( "Content is larger than threshold, archiving data to GCS at '%s' in chunks.", gcsUrl); try (WriteChannel writer = storage.writer(blobInfo)) { for (int chunkOffset = 0; chunkOffset < data.length; chunkOffset += options.chunkSizeInBytes) { int chunkSize = Math.min(data.length - chunkOffset, options.chunkSizeInBytes); writer.write(ByteBuffer.wrap(data, chunkOffset, chunkSize)); } return true; } catch (IOException e) { logger.atSevere().withCause(e).log("Unable to archving data to GCS at '%s'.", gcsUrl); return false; } } /** The factory of {@link GoogleCloudStorageArchiver} types for usage with assisted injection. */ // TODO(b/145315535): consider wrap the Storage API into a client library. Current implementation // is not easily testable. public interface Factory { GoogleCloudStorageArchiver create(Storage storage); } } ================================================ FILE: common/src/main/java/com/google/tsunami/common/io/archiving/GoogleCloudStorageArchiverModule.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.io.archiving; import com.google.inject.AbstractModule; import com.google.inject.assistedinject.FactoryModuleBuilder; /** Installs {@link GoogleCloudStorageArchiver}. */ public class GoogleCloudStorageArchiverModule extends AbstractModule { @Override protected void configure() { install(new FactoryModuleBuilder().build(GoogleCloudStorageArchiver.Factory.class)); } } ================================================ FILE: common/src/main/java/com/google/tsunami/common/io/archiving/RawFileArchiver.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.io.archiving; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import com.google.common.base.Strings; import com.google.common.flogger.GoogleLogger; import com.google.common.io.Files; import java.io.File; import java.io.IOException; /** An {@link Archiver} implementation that archives data into file systems as raw files. */ public class RawFileArchiver implements Archiver { private static final GoogleLogger logger = GoogleLogger.forEnclosingClass(); @Override public boolean archive(String fileName, byte[] data) { checkArgument(!Strings.isNullOrEmpty(fileName)); checkNotNull(data); try { logger.atInfo().log("Archiving data to file system with filename '%s'.", fileName); Files.asByteSink(new File(fileName)).write(data); return true; } catch (IOException e) { logger.atWarning().withCause(e).log("Failed archiving data to file '%s'.", fileName); return false; } } } ================================================ FILE: common/src/main/java/com/google/tsunami/common/io/archiving/testing/FakeArchiver.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.io.archiving.testing; import static com.google.common.truth.Truth.assertThat; import com.google.common.collect.Maps; import com.google.tsunami.common.io.archiving.Archiver; import java.util.Collection; import java.util.Map; import java.util.NoSuchElementException; import java.util.Set; /** An implementation of {@link Archiver} that stores data in memory for testing purposes. */ public final class FakeArchiver implements Archiver { private final Map archivedByteArrayData = Maps.newHashMap(); private final Map archivedCharSequenceData = Maps.newHashMap(); private boolean shouldFail = false; @Override public boolean archive(String name, byte[] data) { if (shouldFail) { return false; } archivedByteArrayData.put(name, data); return true; } @Override public boolean archive(String name, CharSequence data) { if (shouldFail) { return false; } archivedCharSequenceData.put(name, data); return true; } public void failArchival() { this.shouldFail = true; } public byte[] getStoredByteArrays(String name) { if (!archivedByteArrayData.containsKey(name)) { throw new NoSuchElementException(String.format("'%s' not found in FakeArchiver", name)); } return archivedByteArrayData.get(name); } public CharSequence getStoredCharSequence(String name) { if (!archivedCharSequenceData.containsKey(name)) { throw new NoSuchElementException(String.format("'%s' not found in FakeArchiver", name)); } return archivedCharSequenceData.get(name); } public void assertNoByteArraysStored() { assertThat(archivedByteArrayData).isEmpty(); } public void assertNoCharSequencesStored() { assertThat(archivedCharSequenceData).isEmpty(); } public void assertNoDataStored() { assertNoByteArraysStored(); assertNoCharSequencesStored(); } public void assertByteArraysStored(Map expectedData) { assertThat(archivedByteArrayData).containsExactlyEntriesIn(expectedData); } public void assertByteArraysStoredForNames(Set expectedNames) { assertThat(archivedByteArrayData.keySet()).containsExactlyElementsIn(expectedNames); } public void assertByteArraysStoredWithValues(Collection expectedValues) { assertThat(archivedByteArrayData.values()).containsExactlyElementsIn(expectedValues); } public void assertCharSequencesStored(Map expectedData) { assertThat(archivedCharSequenceData).containsExactlyEntriesIn(expectedData); } public void assertCharSequencesStoredForNames(Set expectedNames) { assertThat(archivedCharSequenceData.keySet()).containsExactlyElementsIn(expectedNames); } public void assertCharSequencesStoredWithValues(Collection expectedValues) { assertThat(archivedCharSequenceData.values()).containsExactlyElementsIn(expectedValues); } } ================================================ FILE: common/src/main/java/com/google/tsunami/common/io/archiving/testing/FakeGoogleCloudStorageArchivers.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.io.archiving.testing; import com.google.cloud.storage.Storage; import com.google.tsunami.common.io.archiving.GoogleCloudStorageArchiver; import java.util.HashMap; import java.util.Map; import java.util.NoSuchElementException; /** A collection of fake {@link GoogleCloudStorageArchiver} created by {@link FakeFactory}. */ public final class FakeGoogleCloudStorageArchivers { private final Map delegatedArchivers = new HashMap<>(); public void assertNoDataStored() { for (FakeArchiver delegate : delegatedArchivers.values()) { delegate.assertNoDataStored(); } } /** * Get the byte array data stored in {@code storage} at {@code gcsUrl}. * * @param storage the instance of the GCS storage. * @param gcsUrl the URL to the GCS storage object. * @return the content of the GCS storage object in byte array format. */ public byte[] getStoredByteArrays(Storage storage, String gcsUrl) { if (!delegatedArchivers.containsKey(storage)) { throw new NoSuchElementException(String.format("Storage '%s' not found", storage)); } return delegatedArchivers.get(storage).getStoredByteArrays(gcsUrl); } /** * Get the {@link CharSequence} data stored in {@code storage} at {@code gcsUrl}. * * @param storage the instance of the GCS storage. * @param gcsUrl the URL to the GCS storage object. * @return the content of the GCS storage object in {@link CharSequence} format. */ public CharSequence getStoredCharSequence(Storage storage, String gcsUrl) { if (!delegatedArchivers.containsKey(storage)) { throw new NoSuchElementException(String.format("Storage '%s' not found", storage)); } return delegatedArchivers.get(storage).getStoredCharSequence(gcsUrl); } final class FakeGoogleCloudStorageArchiver extends GoogleCloudStorageArchiver { private final Storage storage; private FakeGoogleCloudStorageArchiver(Storage storage) { super(new Options(), storage); this.storage = storage; } @Override public boolean archive(String gcsUrl, byte[] data) { FakeArchiver fakeArchiver = delegatedArchivers.computeIfAbsent(storage, unused -> new FakeArchiver()); return fakeArchiver.archive(gcsUrl, data); } @Override public boolean archive(String gcsUrl, CharSequence data) { FakeArchiver fakeArchiver = delegatedArchivers.computeIfAbsent(storage, unused -> new FakeArchiver()); return fakeArchiver.archive(gcsUrl, data); } } final class FakeFactory implements GoogleCloudStorageArchiver.Factory { @Override public GoogleCloudStorageArchiver create(Storage storage) { return new FakeGoogleCloudStorageArchiver(storage); } } } ================================================ FILE: common/src/main/java/com/google/tsunami/common/io/archiving/testing/FakeGoogleCloudStorageArchiversModule.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.io.archiving.testing; import com.google.inject.AbstractModule; import com.google.inject.Provides; import com.google.tsunami.common.io.archiving.GoogleCloudStorageArchiver; import javax.inject.Singleton; /** Installs fake factory for {@link GoogleCloudStorageArchiver}. */ public final class FakeGoogleCloudStorageArchiversModule extends AbstractModule { @Provides @Singleton GoogleCloudStorageArchiver.Factory provideGoogleCloudStorageArchiverFactory( FakeGoogleCloudStorageArchivers fakeGoogleCloudStorageArchivers) { return fakeGoogleCloudStorageArchivers.new FakeFactory(); } @Provides @Singleton FakeGoogleCloudStorageArchivers provideFakeGoogleCloudStorageArchivers() { return new FakeGoogleCloudStorageArchivers(); } } ================================================ FILE: common/src/main/java/com/google/tsunami/common/io/archiving/testing/FakeRawFileArchiver.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.io.archiving.testing; import com.google.tsunami.common.io.archiving.RawFileArchiver; /** A fake implementation of the {@link RawFileArchiver}. */ public final class FakeRawFileArchiver extends RawFileArchiver { private final FakeArchiver delegate = new FakeArchiver(); @Override public boolean archive(String fileName, byte[] data) { return delegate.archive(fileName, data); } @Override public boolean archive(String fileName, CharSequence data) { return delegate.archive(fileName, data); } public byte[] getStoredByteArrays(String fileName) { return delegate.getStoredByteArrays(fileName); } public CharSequence getStoredCharSequence(String fileName) { return delegate.getStoredCharSequence(fileName); } public void assertNoDataStored() { delegate.assertNoDataStored(); } } ================================================ FILE: common/src/main/java/com/google/tsunami/common/io/archiving/testing/FakeRawFileArchiverModule.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.io.archiving.testing; import com.google.inject.AbstractModule; import com.google.tsunami.common.io.archiving.RawFileArchiver; import javax.inject.Singleton; /** Installs {@link FakeRawFileArchiver}. */ public final class FakeRawFileArchiverModule extends AbstractModule { @Override protected void configure() { // This is intentional to create 2 separate bindings. One is for FakeRawFileArchiver itself, // which always injects as a singleton. The other one links the binding for RawFileArchiver to // FakeRawFileArchiver so that the FakeRawFileArchiver singleton instance is injected to // RawFileArchiver. This way the classes on the inheritance chain always get the same instance. // // This is useful in unit test. Test cases now are able to get the same injected instance as the // code under test. bind(FakeRawFileArchiver.class).in(Singleton.class); bind(RawFileArchiver.class).to(FakeRawFileArchiver.class); } } ================================================ FILE: common/src/main/java/com/google/tsunami/common/net/FuzzingUtils.java ================================================ /* * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.net; import static com.google.common.base.Strings.isNullOrEmpty; import static com.google.common.collect.ImmutableList.toImmutableList; import static java.util.stream.Collectors.joining; import com.google.auto.value.AutoValue; import com.google.common.base.Splitter; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.tsunami.common.net.http.HttpRequest; import java.net.URI; import java.util.ArrayList; import java.util.List; import java.util.Optional; /** Fuzzing utilities for HTTP request properties. */ public final class FuzzingUtils { /* TODO(b/251480660): Refactor to generic fuzzing library. */ /** * Fuzz GET parameters by replacing values with the provided payload. If no GET parameter is * found, add a new parameter called {@code defaultParameter}. */ public static ImmutableList fuzzGetParametersWithDefaultParameter( HttpRequest request, String payload, String defaultParameter) { return fuzzGetParameters(request, payload, Optional.of(defaultParameter), ImmutableSet.of()); } /** * Fuzz GET parameters by replacing values with the provided payload. Payloads are expected to * represent paths. If encountered, file extesions and path prefixes are kept and provided via * additional exploit requests. If no GET parameter is found, return an empty list. */ public static ImmutableList fuzzGetParametersExpectingPathValues( HttpRequest request, String payload) { return fuzzGetParameters( request, payload, Optional.empty(), ImmutableSet.of(FuzzingModifier.FUZZING_PATHS)); } /** * Fuzz GET parameters by replacing values with the provided payload. If no GET parameter is * found, return an empty list. */ public static ImmutableList fuzzGetParameters(HttpRequest request, String payload) { return fuzzGetParameters(request, payload, Optional.empty(), ImmutableSet.of()); } private static ImmutableList fuzzGetParameters( HttpRequest request, String payload, Optional defaultParameter, ImmutableSet modifiers) { URI parsedUrl = URI.create(request.url()); ImmutableList queryParams = parseQuery(parsedUrl.getQuery()); if (queryParams.isEmpty() && defaultParameter.isPresent()) { return ImmutableList.of( request.toBuilder() .setUrl( assembleUrlWithQueries( parsedUrl, ImmutableList.of(HttpQueryParameter.create(defaultParameter.get(), payload)))) .build()); } return fuzzParams(queryParams, payload, modifiers).stream() .map(fuzzedParams -> assembleUrlWithQueries(parsedUrl, fuzzedParams)) .map(fuzzedUrl -> request.toBuilder().setUrl(fuzzedUrl).build()) .collect(toImmutableList()); } private static ImmutableList setFuzzedParams( ImmutableList params, int index, String payload) { List paramsWithPayload = new ArrayList<>(params); paramsWithPayload.set(index, HttpQueryParameter.create(params.get(index).name(), payload)); return ImmutableList.copyOf(paramsWithPayload); } private static void fuzzParamsWithExtendedPathPayloads( ImmutableSet.Builder> builder, ImmutableList params, int index, String payload) { int dotLocation = params.get(index).value().lastIndexOf('.'); if (dotLocation != -1) { builder.add( setFuzzedParams( params, index, payload + "%00" + params.get(index).value().substring(dotLocation))); } int slashLocation = params.get(index).value().lastIndexOf('/'); if (slashLocation != -1) { builder.add( setFuzzedParams( params, index, params.get(index).value().substring(0, slashLocation + 1) + payload)); } if (dotLocation != -1 && slashLocation != -1 && slashLocation < dotLocation) { builder.add( setFuzzedParams( params, index, params.get(index).value().substring(0, slashLocation + 1) + payload + "%00" + params.get(index).value().substring(dotLocation))); } } private static ImmutableSet> fuzzParams( ImmutableList params, String payload, ImmutableSet modifiers) { ImmutableSet.Builder> fuzzedParamsBuilder = ImmutableSet.builder(); for (int i = 0; i < params.size(); i++) { fuzzedParamsBuilder.add(setFuzzedParams(params, i, payload)); if (modifiers.contains(FuzzingModifier.FUZZING_PATHS)) { fuzzParamsWithExtendedPathPayloads(fuzzedParamsBuilder, params, i, payload); } } return fuzzedParamsBuilder.build(); } public static ImmutableList parseQuery(String query) { if (isNullOrEmpty(query)) { return ImmutableList.of(); } ImmutableList.Builder queryParamsBuilder = ImmutableList.builder(); for (String param : Splitter.on('&').split(query)) { int equalPosition = param.indexOf("="); if (equalPosition > -1) { String name = param.substring(0, equalPosition); String value = param.substring(equalPosition + 1); queryParamsBuilder.add(HttpQueryParameter.create(name, value)); } else { queryParamsBuilder.add(HttpQueryParameter.create(param, "")); } } return queryParamsBuilder.build(); } private static String assembleUrlWithQueries( URI parsedUrl, ImmutableList params) { String query = assembleQueryParams(params); StringBuilder urlBuilder = new StringBuilder(); urlBuilder.append(parsedUrl.getScheme()).append("://").append(parsedUrl.getRawAuthority()); if (!isNullOrEmpty(parsedUrl.getRawPath())) { urlBuilder.append(parsedUrl.getRawPath()); } if (!isNullOrEmpty(query)) { urlBuilder.append('?').append(query); } if (!isNullOrEmpty(parsedUrl.getRawFragment())) { urlBuilder.append('#').append(parsedUrl.getRawFragment()); } return urlBuilder.toString(); } private static String assembleQueryParams(ImmutableList params) { return params.stream() .map(param -> String.format("%s=%s", param.name(), param.value())) .collect(joining("&")); } /** URL Query parameter name and value pair. */ @AutoValue public abstract static class HttpQueryParameter { public abstract String name(); public abstract String value(); public static HttpQueryParameter create(String name, String value) { return new AutoValue_FuzzingUtils_HttpQueryParameter(name, value); } } enum FuzzingModifier { FUZZING_PATHS; } private FuzzingUtils() {} } ================================================ FILE: common/src/main/java/com/google/tsunami/common/net/UrlUtils.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.net; import static java.nio.charset.StandardCharsets.UTF_8; import com.google.common.base.Joiner; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.util.List; import java.util.Optional; import java.util.regex.Pattern; import okhttp3.HttpUrl; /** Utilities for dealing with URLs. */ public final class UrlUtils { private static final Joiner PATH_JOINER = Joiner.on("/"); private static final Pattern SLASH_PREFIX_PATTERN = Pattern.compile("^/+"); private static final Pattern TRAILING_SLASH_PATTERN = Pattern.compile("/+$"); /** * Enumerates all sub-paths for a given URL. All query parameters and fragments are removed. * *

For example: * *

    *
  • given "http://localhost/", it returns ["http://localhost/"] *
  • given "http://localhost/a/b/", it returns * ["http://localhost/", "http://localhost/a/", "http://localhost/a/b/"] *
* * @param url the URL to be enumerated. * @return all sub-paths URLs for the given URL. */ public static ImmutableSet allSubPaths(String url) { return allSubPaths(HttpUrl.parse(url)); } /** * Enumerates all sub-paths for a given URL. All query parameters and fragments are removed. * *

For example: * *

    *
  • given "http://localhost/", it returns ["http://localhost/"] *
  • given "http://localhost/a/b/", it returns * ["http://localhost/", "http://localhost/a/", "http://localhost/a/b/"] *
* * @param url the URL to be enumerated. * @return all sub-paths URLs for the given URL. */ public static ImmutableSet allSubPaths(HttpUrl url) { if (url == null) { return ImmutableSet.of(); } // Url at root. List pathSegments = url.encodedPathSegments(); if (pathSegments.size() == 1 && pathSegments.get(0).isEmpty()) { return ImmutableSet.of(url.newBuilder().query(null).fragment(null).build()); } // Url has sub-paths. ImmutableSet.Builder allSubUrlsBuilder = ImmutableSet.builder(); for (int pathEnd = 0; pathEnd <= pathSegments.size(); pathEnd++) { List subPathSegments = Lists.newArrayList(pathSegments.subList(0, pathEnd)); // Ensure sub-path has leading slash. if (subPathSegments.isEmpty() || !subPathSegments.get(0).isEmpty()) { subPathSegments.add(0, ""); } // Ensure sub-path has trailing slash. if (subPathSegments.size() == 1 || !Iterables.getLast(subPathSegments).isEmpty()) { subPathSegments.add(""); } allSubUrlsBuilder.add( url.newBuilder() .encodedPath(PATH_JOINER.join(subPathSegments)) .query(null) .fragment(null) .build()); } return allSubUrlsBuilder.build(); } /** * Removes the leading slashes of a URL path. * * @param path the URL path to be transformed. * @return a URL path without leading slash. */ public static String removeLeadingSlashes(String path) { return SLASH_PREFIX_PATTERN.matcher(path).replaceFirst(""); } /** * Removes the trailing slashes of a URL path. * * @param path the URL path to be transformed. * @return a URL path without leading slash. */ public static String removeTrailingSlashes(String path) { return TRAILING_SLASH_PATTERN.matcher(path).replaceFirst(""); } /** * Encodes the given String using URL-encoding. * * @param raw the raw String to be encoded. * @return the URL-encoded version of the provided String if it was valid UTF-8. */ public static Optional urlEncode(String raw) { try { return Optional.of(URLEncoder.encode(raw, UTF_8.toString())); } catch (UnsupportedEncodingException e) { return Optional.empty(); } } private UrlUtils() {} } ================================================ FILE: common/src/main/java/com/google/tsunami/common/net/db/ConnectionProvider.java ================================================ /* * Copyright 2023 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.net.db; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; /** A client library that communicates with different databases via jdbc. */ public class ConnectionProvider implements ConnectionProviderInterface { public ConnectionProvider() {} @Override public Connection getConnection(String url, String user, String password) throws SQLException { return DriverManager.getConnection(url, user, password); } } ================================================ FILE: common/src/main/java/com/google/tsunami/common/net/db/ConnectionProviderInterface.java ================================================ /* * Copyright 2023 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.net.db; import java.sql.Connection; import java.sql.SQLException; /** A client interface that communicates with different databases. */ public interface ConnectionProviderInterface { public Connection getConnection(String url, String user, String password) throws SQLException; } ================================================ FILE: common/src/main/java/com/google/tsunami/common/net/http/HttpClient.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.net.http; import com.google.common.util.concurrent.ListenableFuture; import com.google.tsunami.proto.NetworkService; import java.io.IOException; import java.time.Duration; import org.checkerframework.checker.nullness.qual.Nullable; /** A client library that communicates with remote servers via the HTTP protocol. */ public abstract class HttpClient { public static final String TSUNAMI_USER_AGENT = "TsunamiSecurityScanner"; /** * Gets log id. * * @return log id string. */ public abstract String getLogId(); /** * NOTE: This is a temporary hack to workaround OkHttp's hardcoded URL canonicalization algorithm. * We should rewrite the entire library using a more flexible backend. * *

Sends the given HTTP request as is, blocking until full response is received. * * @param httpRequest the HTTP request to be sent by this client. * @return the response returned from the HTTP server. * @throws IOException if an I/O error occurs during the HTTP request. */ public abstract HttpResponse sendAsIs(HttpRequest httpRequest) throws IOException; /** * Sends the given HTTP request using this client, blocking until full response is received. * * @param httpRequest the HTTP request to be sent by this client. * @return the response returned from the HTTP server. * @throws IOException if an I/O error occurs during the HTTP request. */ public abstract HttpResponse send(HttpRequest httpRequest) throws IOException; /** * Sends the given HTTP request using this client blocking until full response is received. If * {@code networkService} is not null, the host header is set according to the service's header * field even if it resolves to a different ip. * * @param httpRequest the HTTP request to be sent by this client. * @param networkService the {@link NetworkService} proto to be used for the HOST header. * @return the response returned from the HTTP server. * @throws IOException if an I/O error occurs during the HTTP request. */ public abstract HttpResponse send( HttpRequest httpRequest, @Nullable NetworkService networkService) throws IOException; /** * Sends the given HTTP request using this client asynchronously. * * @param httpRequest the HTTP request to be sent by this client. * @return the future for the response to be returned from the HTTP server. */ public abstract ListenableFuture sendAsync(HttpRequest httpRequest); /** * Sends the given HTTP request using this client asynchronously. If {@code networkService} is not * null, the host header is set according to the service's header field even if it resolves to a * different ip. * * @param httpRequest the HTTP request to be sent by this client. * @param networkService the {@link NetworkService} proto to be used for the HOST header. * @return the future for the response to be returned from the HTTP server. */ public abstract ListenableFuture sendAsync( HttpRequest httpRequest, @Nullable NetworkService networkService); public abstract Builder modify(); /** Base builder for implementations of HttpClient */ public abstract static class Builder { public abstract Builder setFollowRedirects(boolean followRedirects); public abstract Builder setLogId(String logId); public abstract Builder setConnectTimeout(Duration connectionTimeout); public abstract T build(); } } ================================================ FILE: common/src/main/java/com/google/tsunami/common/net/http/HttpClientCliOptions.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.net.http; import com.beust.jcommander.Parameter; import com.beust.jcommander.ParameterException; import com.beust.jcommander.Parameters; import com.google.tsunami.common.cli.CliOption; import org.checkerframework.checker.nullness.qual.Nullable; /** Command line argument for {@link HttpClient}. */ @Parameters(separators = "=") public final class HttpClientCliOptions implements CliOption { @Parameter( names = "--http-client-trust-all-certificates", arity = 1, description = "Whether the HTTP client should trust all certificates on HTTPS traffic.") public Boolean trustAllCertificates; @Parameter( names = "--http-client-call-timeout-seconds", description = "[Depreciated] Set to be the same as the timeout specified by" + " --http-client-connect-timeout-seconds.") Integer callTimeoutSeconds; @Parameter( names = "--http-client-connect-timeout-seconds", description = "The timeout in seconds for new HTTP connections. See" + " https://square.github.io/okhttp/4.x/okhttp/okhttp3/-ok-http-client/-builder/connect-timeout/" + " for more details.") Integer connectTimeoutSeconds; @Parameter( names = "--http-client-read-timeout-seconds", description = "[Depreciated] Set to be the same as the timeout specified by" + " --http-client-connect-timeout-seconds") Integer readTimeoutSeconds; @Parameter( names = "--http-client-write-timeout-seconds", description = "[Depreciated] Set to be the same as the timeout specified by" + " --http-client-connect-timeout-seconds.") Integer writeTimeoutSeconds; @Parameter( names = "--http-client-user-agent", description = "User-Agent to use in HTTP requests.") public String userAgent = HttpClient.TSUNAMI_USER_AGENT; @Override public void validate() { validateTimeout("--http-client-call-timeout-seconds", callTimeoutSeconds); validateTimeout("--http-client-connect-timeout-seconds", connectTimeoutSeconds); validateTimeout("--http-client-read-timeout-seconds", readTimeoutSeconds); validateTimeout("--http-client-write-timeout-seconds", writeTimeoutSeconds); } private static void validateTimeout(String flagName, @Nullable Integer value) { if (value != null && value < 0) { throw new ParameterException( String.format("%s cannot be a negative number, received %d.", flagName, value)); } } } ================================================ FILE: common/src/main/java/com/google/tsunami/common/net/http/HttpClientConfigProperties.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.net.http; import com.google.tsunami.common.config.annotations.ConfigProperties; /** Configuration properties for {@link HttpClient}. */ @ConfigProperties("common.net.http") public final class HttpClientConfigProperties { /** Whether the HTTP client should trust all certificates on HTTPS traffic. */ Boolean trustAllCertificates; /** * The timeout in seconds for complete HTTP calls. See * https://square.github.io/okhttp/4.x/okhttp/okhttp3/-ok-http-client/-builder/call-timeout/ for * more details. */ Integer callTimeoutSeconds; /** * The timeout in seconds for new HTTP connections. See * https://square.github.io/okhttp/4.x/okhttp/okhttp3/-ok-http-client/-builder/connect-timeout/ * for more details. */ Integer connectTimeoutSeconds; /** * The timeout in seconds for the read operations for HTTP connections. See * https://square.github.io/okhttp/4.x/okhttp/okhttp3/-ok-http-client/-builder/read-timeout/ for * more details. */ Integer readTimeoutSeconds; /** * The timeout in seconds for the write operations for HTTP connections. See * https://square.github.io/okhttp/4.x/okhttp/okhttp3/-ok-http-client/-builder/write-timeout/ for * more details. */ Integer writeTimeoutSeconds; } ================================================ FILE: common/src/main/java/com/google/tsunami/common/net/http/HttpClientModule.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.net.http; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Strings.isNullOrEmpty; import static java.util.concurrent.TimeUnit.MILLISECONDS; import com.google.inject.AbstractModule; import com.google.inject.Provides; import com.google.tsunami.common.net.http.javanet.ConnectionFactory; import com.google.tsunami.common.net.http.javanet.DefaultConnectionFactory; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.security.GeneralSecurityException; import java.security.SecureRandom; import java.security.cert.X509Certificate; import java.time.Duration; import javax.inject.Qualifier; import javax.inject.Singleton; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; import okhttp3.ConnectionPool; import okhttp3.Dispatcher; import okhttp3.OkHttpClient; /** Guice module for installing {@link HttpClient} library. */ public final class HttpClientModule extends AbstractModule { // This TrustManager does NOT validate certificate chains. private static final X509TrustManager TRUST_ALL_CERTS_MANAGER = new X509TrustManager() { @Override public void checkClientTrusted(X509Certificate[] chain, String authType) {} @Override public void checkServerTrusted(X509Certificate[] chain, String authType) {} @Override public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; } }; // Maximum number of requests for each host (URL's host name) to execute concurrently. private static final int OKHTTPCLIENT_MAX_REQUESTS_PER_HOST = 5; // Maximum number of idle connections to each to keep in the pool. private final int connectionPoolMaxIdle; // Duration to keep the connection alive in the pool before closing it. private final Duration connectionPoolKeepAliveDuration; // Maximum number of requests to execute concurrently. private final int maxRequests; // Whether or not to follow redirect from server. private final boolean followRedirects; // A log ID to print in front of the logs. private final String logId; public HttpClientModule(Builder builder) { checkNotNull(builder); this.connectionPoolMaxIdle = builder.connectionPoolMaxIdle; this.connectionPoolKeepAliveDuration = builder.connectionPoolKeepAliveDuration; this.maxRequests = builder.maxRequests; this.followRedirects = builder.followRedirects; this.logId = builder.logId; } @Provides @Singleton ConnectionPool provideConnectionPool() { return new ConnectionPool( connectionPoolMaxIdle, connectionPoolKeepAliveDuration.toMillis(), MILLISECONDS); } @Provides @Singleton Dispatcher provideDispatcher() { Dispatcher dispatcher = new Dispatcher(); dispatcher.setMaxRequests(maxRequests); dispatcher.setMaxRequestsPerHost(OKHTTPCLIENT_MAX_REQUESTS_PER_HOST); return dispatcher; } @Provides @Singleton @TrustAllCertsSocketFactory SSLSocketFactory provideTrustAllCertsSocketFactory() throws GeneralSecurityException { SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(null, new TrustManager[] {TRUST_ALL_CERTS_MANAGER}, new SecureRandom()); return sslContext.getSocketFactory(); } // Missing features: // 1. Custom cookie handler. @Provides @Singleton OkHttpClient provideOkHttpClient( ConnectionPool connectionPool, Dispatcher dispatcher, @TrustAllCertsSocketFactory SSLSocketFactory trustAllCertsSocketFactory, @TrustAllCertificates boolean trustAllCertificates, @ConnectTimeoutSeconds int connectTimeoutSeconds) { OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder() .callTimeout(Duration.ofSeconds(connectTimeoutSeconds)) .connectTimeout(Duration.ofSeconds(connectTimeoutSeconds)) .readTimeout(Duration.ofSeconds(connectTimeoutSeconds)) .writeTimeout(Duration.ofSeconds(connectTimeoutSeconds)) .connectionPool(connectionPool) .dispatcher(dispatcher) .followRedirects(followRedirects); if (trustAllCertificates) { clientBuilder .sslSocketFactory(trustAllCertsSocketFactory, TRUST_ALL_CERTS_MANAGER) .hostnameVerifier((hostname, session) -> true); } return clientBuilder.build(); } @Provides @Singleton HttpClient provideOkHttpHttpClient( OkHttpClient okHttpClient, @TrustAllCertificates boolean trustAllCertificates, ConnectionFactory connectionFactory, @LogId String logId, @ConnectTimeout Duration connectTimeout, @UserAgent String userAgent) { return new OkHttpHttpClient( okHttpClient, trustAllCertificates, connectionFactory, logId, connectTimeout, userAgent); } @Provides @Singleton ConnectionFactory provideJavaNetConnectionFactory( @TrustAllCertificates boolean trustAllCertificates, @TrustAllCertsSocketFactory SSLSocketFactory trustAllCertsSocketFactory, @ConnectTimeoutSeconds int connectTimeoutSeconds, @ReadTimeoutSeconds int readTimeoutSeconds) { return new DefaultConnectionFactory( trustAllCertificates, trustAllCertsSocketFactory, Duration.ofSeconds(connectTimeoutSeconds), Duration.ofSeconds(readTimeoutSeconds)); } @Provides @TrustAllCertificates boolean shouldTrustAllCertificates( HttpClientCliOptions httpClientCliOptions, HttpClientConfigProperties httpClientConfigProperties) { if (httpClientCliOptions.trustAllCertificates != null) { return httpClientCliOptions.trustAllCertificates; } if (httpClientConfigProperties.trustAllCertificates != null) { return httpClientConfigProperties.trustAllCertificates; } return true; } @Provides @LogId String provideLogid() { return logId; } @Provides @FollowRedirects boolean provideFollowRedirects() { return followRedirects; } @Provides @MaxRequests int provideMaxRequests() { return maxRequests; } @Provides @CallTimeoutSeconds int provideCallTimeoutSeconds( HttpClientCliOptions httpClientCliOptions, HttpClientConfigProperties httpClientConfigProperties) { if (httpClientCliOptions.callTimeoutSeconds != null) { return httpClientCliOptions.callTimeoutSeconds; } if (httpClientConfigProperties.callTimeoutSeconds != null) { return httpClientConfigProperties.callTimeoutSeconds; } // Default call timeout specified in // https://square.github.io/okhttp/4.x/okhttp/okhttp3/-ok-http-client/-builder/call-timeout/. return 0; } @Provides @ConnectTimeoutSeconds int provideConnectTimeoutSeconds( HttpClientCliOptions httpClientCliOptions, HttpClientConfigProperties httpClientConfigProperties) { if (httpClientCliOptions.connectTimeoutSeconds != null) { return httpClientCliOptions.connectTimeoutSeconds; } if (httpClientConfigProperties.connectTimeoutSeconds != null) { return httpClientConfigProperties.connectTimeoutSeconds; } // Default connect timeout specified in // https://square.github.io/okhttp/4.x/okhttp/okhttp3/-ok-http-client/-builder/connect-timeout/. return 10; } @Provides @ConnectTimeout Duration provideConnectTimeout(@ConnectTimeoutSeconds int connectionTimeoutSeconds) { return Duration.ofSeconds(connectionTimeoutSeconds); } @Provides @ReadTimeoutSeconds int provideReadTimeoutSeconds( HttpClientCliOptions httpClientCliOptions, HttpClientConfigProperties httpClientConfigProperties) { if (httpClientCliOptions.readTimeoutSeconds != null) { return httpClientCliOptions.readTimeoutSeconds; } if (httpClientConfigProperties.readTimeoutSeconds != null) { return httpClientConfigProperties.readTimeoutSeconds; } // Default read timeout specified in // https://square.github.io/okhttp/4.x/okhttp/okhttp3/-ok-http-client/-builder/read-timeout/. return 10; } @Provides @WriteTimeoutSeconds int provideWriteTimeoutSeconds( HttpClientCliOptions httpClientCliOptions, HttpClientConfigProperties httpClientConfigProperties) { if (httpClientCliOptions.writeTimeoutSeconds != null) { return httpClientCliOptions.writeTimeoutSeconds; } if (httpClientConfigProperties.writeTimeoutSeconds != null) { return httpClientConfigProperties.writeTimeoutSeconds; } // Default write timeout specified in // https://square.github.io/okhttp/4.x/okhttp/okhttp3/-ok-http-client/-builder/write-timeout/. return 10; } @Provides @UserAgent String provideUserAgent(HttpClientCliOptions httpClientCliOptions) { if (!isNullOrEmpty(httpClientCliOptions.userAgent)) { return httpClientCliOptions.userAgent; } return HttpClient.TSUNAMI_USER_AGENT; } @Qualifier @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD}) @interface TrustAllCertificates {} @Qualifier @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD}) @interface TrustAllCertsSocketFactory {} @Qualifier @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD}) @interface LogId {} @Qualifier @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD}) @interface CallTimeoutSeconds {} @Qualifier @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD}) @interface ConnectTimeoutSeconds {} @Qualifier @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD}) @interface ConnectTimeout {} @Qualifier @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD}) @interface ReadTimeoutSeconds {} @Qualifier @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD}) @interface WriteTimeoutSeconds {} @Qualifier @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD}) @interface FollowRedirects {} @Qualifier @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD}) @interface MaxRequests {} @Qualifier @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD}) @interface UserAgent {} /** Builder for {@link HttpClientModule}. */ public static final class Builder { private static final int DEFAULT_CONNECTION_POOL_MAX_IDLE = 5; private static final Duration DEFAULT_CONNECTION_POOL_KEEP_ALIVE_DURATION = Duration.ofMinutes(5); private static final int DEFAULT_MAX_REQUESTS = 64; private static final boolean DEFAULT_FOLLOW_REDIRECTS = true; private static final String DEFAULT_LOG_ID = ""; private int connectionPoolMaxIdle = DEFAULT_CONNECTION_POOL_MAX_IDLE; private Duration connectionPoolKeepAliveDuration = DEFAULT_CONNECTION_POOL_KEEP_ALIVE_DURATION; private int maxRequests = DEFAULT_MAX_REQUESTS; private boolean followRedirects = DEFAULT_FOLLOW_REDIRECTS; private String logId = DEFAULT_LOG_ID; /** * Sets the maximum number of idle connections to each to keep in the pool. * * @param maxIdle maximum number of idel connecteds. * @return the {@link Builder} instance itself. */ public Builder setConnectionPoolMaxIdle(int maxIdle) { checkArgument(maxIdle > 0); this.connectionPoolMaxIdle = maxIdle; return this; } /** * Sets the duration to keep the connection alive in the connection pool before closing it. * * @param keepAliveDuration the duration to keep the connection alive. * @return the {@link Builder} instance itself. */ public Builder setConnectionPoolKeepAliveDuration(Duration keepAliveDuration) { checkNotNull(keepAliveDuration); checkArgument(!keepAliveDuration.isNegative()); this.connectionPoolKeepAliveDuration = keepAliveDuration; return this; } /** * Sets the maximum number of requests to execute concurrently. * * @param maxRequests the maximum number of concurrent requests. * @return the {@link Builder} instance itself. */ public Builder setMaxRequests(int maxRequests) { checkArgument(maxRequests > 0); this.maxRequests = maxRequests; return this; } /** * Sets whether or not to follow redirect from server. If unset, by default redirects will be * followed. * * @param followRedirects whether the HTTP client should follow redirect responses from the * server. * @return the {@link Builder} instance itself. */ public Builder setFollowRedirects(boolean followRedirects) { this.followRedirects = followRedirects; return this; } /** * Sets the log ID to print in front of the logs. * * @param logId the log ID to print in front of the logs. * @return the {@link Builder} instance itself. */ public Builder setLogId(String logId) { this.logId = logId; return this; } public HttpClientModule build() { return new HttpClientModule(this); } } } ================================================ FILE: common/src/main/java/com/google/tsunami/common/net/http/HttpHeaders.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.net.http; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import com.google.auto.value.AutoValue; import com.google.common.base.Ascii; import com.google.common.base.CharMatcher; import com.google.common.base.MoreObjects; import com.google.common.collect.ImmutableBiMap; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.ImmutableSet; import com.google.errorprone.annotations.Immutable; import java.lang.reflect.Field; import java.util.Optional; /** Immutable HTTP headers. */ @Immutable @AutoValue public abstract class HttpHeaders { private static final ImmutableBiMap LOWER_TO_KNOWN = createKnownHeaders(); private static final ImmutableSet KNOWN = LOWER_TO_KNOWN.values(); /** Canonicalize a header name. */ private static String canonicalize(String headerName) { if (KNOWN.contains(headerName)) { return headerName; } String lower = Ascii.toLowerCase(headerName); String known = LOWER_TO_KNOWN.get(lower); return MoreObjects.firstNonNull(known, lower); } private static ImmutableBiMap createKnownHeaders() { ImmutableBiMap.Builder builder = ImmutableBiMap.builder(); addFields(builder, com.google.common.net.HttpHeaders.class); return builder.build(); } /** * Loops over all of the public String fields in the given class and puts them into the BiMap * (lower case to original string value). */ private static void addFields(ImmutableBiMap.Builder builder, Class clazz) { try { for (Field field : clazz.getFields()) { if (field.getType().equals(String.class)) { String known = (String) field.get(null); String lower = Ascii.toLowerCase(known); builder.put(lower, known); } } } catch (ReflectiveOperationException e) { throw new IllegalStateException(e); } } abstract ImmutableListMultimap rawHeaders(); /** * Gets a set of all HTTP header names. * * @return all HTTP header names. */ public ImmutableSet names() { return rawHeaders().keySet(); } /** * Returns the first value for the header with the given name, or empty Optional if none exists. * * @param name case-insensitive header name * @return the first value for the given header name. */ public Optional get(String name) { checkNotNull(name, "Name cannot be null."); ImmutableList values = getAll(name); return values.isEmpty() ? Optional.empty() : Optional.of(values.get(0)); } /** * Returns all the values for the header with the given name. Values are in the same order they * were added to the builder. * * @param name case-insensitive header name * @return All values for the given header name. */ public ImmutableList getAll(String name) { checkNotNull(name, "Name cannot be null."); // We first check the multimap using whatever string is passed in. Usually // this will be a constant from HttpHeaders, which is pre-canonicalized. // Only if the lookup fails do we then canonicalize and try again. ImmutableList values = rawHeaders().get(name); if (!values.isEmpty()) { return values; } String fixedName = canonicalize(name); if (fixedName.equals(name)) { return values; // Name was already canonicalized, so return the empty list. } return rawHeaders().get(fixedName); } public static Builder builder() { return new AutoValue_HttpHeaders.Builder(); } /** Builder for {@link HttpHeaders}. */ @AutoValue.Builder public abstract static class Builder { /** RFC 2616 section 4.2. */ private static final CharMatcher HEADER_NAME_MATCHER = CharMatcher.inRange('!', '~').and(CharMatcher.isNot(':')); /** RFC 2616 section 4.2. */ private static final CharMatcher HEADER_VALUE_MATCHER = CharMatcher.inRange((char) 0, (char) 31) // No control characters .or(CharMatcher.is((char) 127)) // or DEL .negate() .or(CharMatcher.is('\t')); // except horizontal-tab abstract ImmutableListMultimap.Builder rawHeadersBuilder(); public Builder addHeader(String name, String value) { checkNotNull(name, "Name cannot be null."); checkNotNull(value, "Value cannot be null."); checkArgument(isLegalHeaderName(name), "Illegal header name %s", name); checkArgument(isLegalHeaderValue(value), "Illegal header value %s", value); rawHeadersBuilder().put(canonicalize(name), value); return this; } public Builder addHeader(String name, String value, boolean canonicalize) { checkNotNull(name, "Name cannot be null."); checkNotNull(value, "Value cannot be null."); if (canonicalize) { return addHeader(name, value); } else { rawHeadersBuilder().put(name, value); return this; } } public abstract HttpHeaders build(); private static boolean isLegalHeaderName(String str) { return HEADER_NAME_MATCHER.matchesAllOf(str); } private static boolean isLegalHeaderValue(String value) { return HEADER_VALUE_MATCHER.matchesAllOf(value); } } } ================================================ FILE: common/src/main/java/com/google/tsunami/common/net/http/HttpMethod.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.net.http; /** Represents HTTP methods. */ public enum HttpMethod { // Add more http methods here if necessary. GET("GET"), HEAD("HEAD"), POST("POST"), PUT("PUT"), DELETE("DELETE"); private final String string; HttpMethod(String string) { this.string = string; } @Override public String toString() { return string; } } ================================================ FILE: common/src/main/java/com/google/tsunami/common/net/http/HttpRequest.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.net.http; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; import com.google.auto.value.AutoValue; import com.google.common.base.Strings; import com.google.errorprone.annotations.Immutable; import com.google.protobuf.ByteString; import java.util.Optional; import okhttp3.HttpUrl; /** Immutable HTTP request. */ @Immutable @AutoValue @AutoValue.CopyAnnotations @SuppressWarnings("Immutable") public abstract class HttpRequest { public abstract HttpMethod method(); public abstract String url(); public abstract HttpHeaders headers(); public abstract Optional requestBody(); public abstract Builder toBuilder(); /** * Creates a {@link Builder} object for configuring {@link HttpRequest}. * * @return a {@link Builder} instance for the {@link HttpRequest} object. */ public static Builder builder() { return new AutoValue_HttpRequest.Builder(); } /** * Create a new HTTP GET request with the given {@code url}. * * @param url the url of the GET request. * @return a {@link Builder} object for configuring {@link HttpRequest}. */ public static Builder get(String url) { checkArgument(!Strings.isNullOrEmpty(url)); return builder().setMethod(HttpMethod.GET).setUrl(url); } /** * Create a new HTTP GET request with the given {@code uri}. * * @param uri the url of the GET request. * @return a {@link Builder} object for configuring {@link HttpRequest}. */ public static Builder get(HttpUrl uri) { checkNotNull(uri); return builder().setMethod(HttpMethod.GET).setUrl(uri); } /** * Create a new HTTP HEAD request with the given {@code url}. * * @param url the url of the HEAD request. * @return a {@link Builder} object for configuring {@link HttpRequest}. */ public static Builder head(String url) { checkArgument(!Strings.isNullOrEmpty(url)); return builder().setMethod(HttpMethod.HEAD).setUrl(url); } /** * Create a new HTTP HEAD request with the given {@code uri}. * * @param uri the url of the HEAD request. * @return a {@link Builder} object for configuring {@link HttpRequest}. */ public static Builder head(HttpUrl uri) { checkNotNull(uri); return builder().setMethod(HttpMethod.HEAD).setUrl(uri); } /** * Create a new HTTP POST request with the given {@code url}. * * @param url the url of the POST request. * @return a {@link Builder} object for configuring {@link HttpRequest}. */ public static Builder post(String url) { checkArgument(!Strings.isNullOrEmpty(url)); return builder().setMethod(HttpMethod.POST).setUrl(url); } /** * Create a new HTTP POST request with the given {@code uri}. * * @param uri the url of the POST request. * @return a {@link Builder} object for configuring {@link HttpRequest}. */ public static Builder post(HttpUrl uri) { checkNotNull(uri); return builder().setMethod(HttpMethod.POST).setUrl(uri); } /** * Create a new HTTP PUT request with the given {@code url}. * * @param url the url of the PUT request. * @return a {@link Builder} object for configuring {@link HttpRequest}. */ public static Builder put(String url) { checkArgument(!Strings.isNullOrEmpty(url)); return put(HttpUrl.parse(url)); } /** * Create a new HTTP PUT request with the given {@code uri}. * * @param uri the url of the PUT request. * @return a {@link Builder} object for configuring {@link HttpRequest}. */ public static Builder put(HttpUrl uri) { checkNotNull(uri); return builder().setMethod(HttpMethod.PUT).setUrl(uri); } /** * Create a new HTTP DELETE request with the given {@code url}. * * @param url the url of the DELETE request. * @return a {@link Builder} object for configuring {@link HttpRequest}. */ public static Builder delete(String url) { checkArgument(!Strings.isNullOrEmpty(url)); return builder().setMethod(HttpMethod.DELETE).setUrl(url); } /** * Create a new HTTP DELETE request with the given {@code uri}. * * @param uri the url of the DELETE request. * @return a {@link Builder} object for configuring {@link HttpRequest}. */ public static Builder delete(HttpUrl uri) { checkNotNull(uri); return builder().setMethod(HttpMethod.DELETE).setUrl(uri); } /** Builder for {@link HttpRequest}. */ @AutoValue.Builder public abstract static class Builder { public abstract Builder setMethod(HttpMethod method); public abstract Builder setUrl(String url); public Builder setUrl(HttpUrl url) { setUrl(url.toString()); return this; } public abstract Builder setHeaders(HttpHeaders httpHeaders); public abstract Builder setRequestBody(ByteString requestBody); public abstract Builder setRequestBody(Optional requestBody); public Builder withEmptyHeaders() { setHeaders(HttpHeaders.builder().build()); return this; } abstract HttpRequest autoBuild(); public HttpRequest build() { HttpRequest httpRequest = autoBuild(); switch (httpRequest.method()) { case GET: case HEAD: checkState( !httpRequest.requestBody().isPresent(), "A request body is not allowed for HTTP GET/HEAD request"); break; case POST: case PUT: case DELETE: break; } return httpRequest; } } } ================================================ FILE: common/src/main/java/com/google/tsunami/common/net/http/HttpResponse.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.net.http; import com.google.auto.value.AutoValue; import com.google.auto.value.extension.memoized.Memoized; import com.google.errorprone.annotations.Immutable; import com.google.gson.JsonElement; import com.google.gson.JsonParser; import com.google.gson.JsonPrimitive; import com.google.protobuf.ByteString; import java.util.Optional; import okhttp3.HttpUrl; /** Immutable HTTP response. */ @Immutable @AutoValue @AutoValue.CopyAnnotations // HttpUrl is immutable even if not marked as such. @SuppressWarnings("Immutable") public abstract class HttpResponse { public abstract HttpStatus status(); public abstract HttpHeaders headers(); public abstract Optional bodyBytes(); // The URL that produced this response. // TODO(b/173574468): Provide the full redirection request not just the Url. public abstract Optional responseUrl(); /** * Gets the body of the HTTP response as a UTF-8 encoded String. * * @return HTTP response body as a Java {@link String}. */ @Memoized public Optional bodyString() { return bodyBytes().map(ByteString::toStringUtf8); } /** * Tries to parse the response body as json and returns the parsing result as {@link JsonElement}. * If parsing failed, an empty optional is returned. * * @return HTTP response body as a Gson {@link JsonElement} object. */ @Memoized public Optional bodyJson() { try { return bodyString().map(JsonParser::parseString); } catch (RuntimeException e) { // Do best-effort parsing and ignore Json parsing errors return Optional.empty(); } } /** * Tries to determine if a given field in Json response is equal to a specific value. If parsing * failed, {@link com.google.gson.JsonSyntaxException} or {@link IllegalStateException} will be * thrown. * * @return boolean */ public boolean jsonFieldEqualsToValue(String fieldname, String value) { Optional jsonPrimitive = bodyJson() .map(JsonElement::getAsJsonObject) .map(object -> object.getAsJsonPrimitive(fieldname)); return jsonPrimitive.isPresent() && jsonPrimitive.get().getAsString().equals(value); } public static Builder builder() { return new AutoValue_HttpResponse.Builder(); } /** Builder for {@link HttpResponse}. */ @AutoValue.Builder public abstract static class Builder { public abstract Builder setStatus(HttpStatus httpStatus); public abstract Builder setHeaders(HttpHeaders httpHeaders); public abstract Builder setBodyBytes(ByteString bodyBytes); public abstract Builder setBodyBytes(Optional bodyBytes); public abstract Builder setResponseUrl(HttpUrl url); public abstract Builder setResponseUrl(Optional url); public abstract HttpResponse build(); } } ================================================ FILE: common/src/main/java/com/google/tsunami/common/net/http/HttpStatus.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.net.http; import static com.google.common.collect.ImmutableMap.toImmutableMap; import com.google.common.collect.ImmutableMap; import java.util.Arrays; import java.util.function.Function; /** * HTTP Status Codes defined in RFC 2616, RFC 6585, RFC 4918 and RFC 7538. * * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html * @see http://tools.ietf.org/html/rfc6585 * @see https://tools.ietf.org/html/rfc4918 * @see https://tools.ietf.org/html/rfc7538 */ public enum HttpStatus { // Default HTTP_STATUS_UNSPECIFIED(0, "Status Unspecified"), // Informational 1xx CONTINUE(100, "Continue"), SWITCHING_PROTOCOLS(101, "Switching Protocols"), // Successful 2xx OK(200, "Ok"), CREATED(201, "Created"), ACCEPTED(202, "Accepted"), NON_AUTHORITATIVE_INFORMATION(203, "Non-Authoritative Information"), NO_CONTENT(204, "No Content"), RESET_CONTENT(205, "Reset Content"), PARTIAL_CONTENT(206, "Partial Content"), MULTI_STATUS(207, "Multi-Status"), // Redirection 3xx MULTIPLE_CHOICES(300, "Multiple Choices"), MOVED_PERMANENTLY(301, "Moved Permanently"), FOUND(302, "Found"), SEE_OTHER(303, "See Other"), NOT_MODIFIED(304, "Not Modified"), USE_PROXY(305, "Use Proxy"), TEMPORARY_REDIRECT(307, "Temporary Redirect"), PERMANENT_REDIRECT(308, "Permanent Redirect"), // Client Error 4xx BAD_REQUEST(400, "Bad Request"), UNAUTHORIZED(401, "Unauthorized"), PAYMENT_REQUIRED(402, "Payment Required"), FORBIDDEN(403, "Forbidden"), NOT_FOUND(404, "Not Found"), METHOD_NOT_ALLOWED(405, "Method Not Allowed"), NOT_ACCEPTABLE(406, "Not Acceptable"), PROXY_AUTHENTICATION_REQUIRED(407, "Proxy Authentication Required"), REQUEST_TIMEOUT(408, "Request Timeout"), CONFLICT(409, "Conflict"), GONE(410, "Gone"), LENGTH_REQUIRED(411, "Length Required"), PRECONDITION_FAILED(412, "Precondition Failed"), REQUEST_ENTITY_TOO_LARGE(413, "Request Entity Too Large"), REQUEST_URI_TOO_LONG(414, "Request Uri Too Long"), UNSUPPORTED_MEDIA_TYPE(415, "Unsupported Media Type"), REQUEST_RANGE_NOT_SATISFIABLE(416, "Request Range Not Satisfiable"), EXPECTATION_FAILED(417, "Expectation Failed"), UNPROCESSABLE_ENTITY(422, "Unprocessable Entity"), LOCKED(423, "Locked"), FAILED_DEPENDENCY(424, "Failed Dependency"), PRECONDITION_REQUIRED(428, "Precondition Required"), TOO_MANY_REQUESTS(429, "Too Many Requests"), REQUEST_HEADER_FIELDS_TOO_LARGE(431, "Request Header Fields Too Large"), // Server Error 5xx INTERNAL_SERVER_ERROR(500, "Internal Server Error"), NOT_IMPLEMENTED(501, "Not Implemented"), BAD_GATEWAY(502, "Bad Gateway"), SERVICE_UNAVAILABLE(503, "Service Unavailable"), GATEWAY_TIMEOUT(504, "Gateway Timeout"), HTTP_VERSION_NOT_SUPPORTED(505, "Http Version Not Supported"), INSUFFICIENT_STORAGE(507, "Insufficient Storage"), NETWORK_AUTHENTICATION_REQUIRED(511, "Network Authentication Required"), /* * IE returns this code for 204 due to its use of URLMon, which returns this * code for 'Operation Aborted'. The status text is 'Unknown', the response * headers are ''. Known to occur on IE 6 on XP through IE9 on Win7. */ QUIRK_IE_NO_CONTENT(1223, "Quirk IE No Content"); /** Status indexed by code. */ private static final ImmutableMap BY_CODE = Arrays.stream(HttpStatus.values()) .collect(toImmutableMap(HttpStatus::code, Function.identity())); /** * Creates the {@link HttpStatus} from the given status code, or null if there is no known status * with that code. * * @param code the HTTP status code. * @return the matching {@link HttpStatus} from the given status code. */ public static HttpStatus fromCode(int code) { HttpStatus status = BY_CODE.get(code); return status == null ? HTTP_STATUS_UNSPECIFIED : status; } private final int code; private final String name; HttpStatus(int code, String name) { this.code = code; this.name = name; } public int code() { return code; } public boolean isRedirect() { switch (this) { case MULTIPLE_CHOICES: case MOVED_PERMANENTLY: case FOUND: case SEE_OTHER: case TEMPORARY_REDIRECT: case PERMANENT_REDIRECT: return true; default: return false; } } public boolean isSuccess() { return code >= 200 && code < 300; } @Override public String toString() { return name; } } ================================================ FILE: common/src/main/java/com/google/tsunami/common/net/http/OkHttpHttpClient.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.net.http; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Strings.isNullOrEmpty; import static com.google.common.net.HttpHeaders.USER_AGENT; import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import com.google.common.base.Ascii; import com.google.common.collect.ImmutableSet; import com.google.common.flogger.GoogleLogger; import com.google.common.io.ByteSource; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.protobuf.ByteString; import com.google.tsunami.common.net.http.javanet.ConnectionFactory; import com.google.tsunami.proto.NetworkService; import java.io.IOException; import java.net.HttpURLConnection; import java.time.Duration; import java.util.List; import java.util.Map; import javax.net.ssl.HttpsURLConnection; import okhttp3.Call; import okhttp3.Callback; import okhttp3.Dns; import okhttp3.Headers; import okhttp3.MediaType; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; import okhttp3.ResponseBody; import org.checkerframework.checker.nullness.qual.Nullable; /** * A client library that communicates with remote servers via the HTTP protocol using {@link * OkHttpClient}. */ final class OkHttpHttpClient extends HttpClient { private static final GoogleLogger logger = GoogleLogger.forEnclosingClass(); private final OkHttpClient okHttpClient; private final boolean trustAllCertificates; private final ConnectionFactory connectionFactory; private final String logId; private final Duration connectionTimeout; private final String userAgent; OkHttpHttpClient( OkHttpClient okHttpClient, boolean trustAllCertificates, ConnectionFactory connectionFactory, String logId, Duration connectionTimeout, String userAgent) { this.okHttpClient = checkNotNull(okHttpClient); this.trustAllCertificates = trustAllCertificates; this.connectionFactory = checkNotNull(connectionFactory); this.logId = logId; this.connectionTimeout = connectionTimeout; this.userAgent = isNullOrEmpty(userAgent) ? TSUNAMI_USER_AGENT : userAgent; } /** * Gets log id. * * @return log id string. */ @Override public String getLogId() { return this.logId; } /** * NOTE: This is a temporary hack to workaround OkHttp's hardcoded URL canonicalization algorithm. * We should rewrite the entire library using a more flexible backend. * *

Sends the given HTTP request as is, blocking until full response is received. * * @param httpRequest the HTTP request to be sent by this client. * @return the response returned from the HTTP server. * @throws IOException if an I/O error occurs during the HTTP request. */ @Override public HttpResponse sendAsIs(HttpRequest httpRequest) throws IOException { HttpURLConnection connection = connectionFactory.openConnection(httpRequest.url()); connection.setRequestMethod(httpRequest.method().toString()); httpRequest.headers().names().stream() .filter(headerName -> !Ascii.equalsIgnoreCase(headerName, USER_AGENT)) .forEach( headerName -> httpRequest .headers() .getAll(headerName) .forEach( headerValue -> connection.setRequestProperty(headerName, headerValue))); connection.setRequestProperty(USER_AGENT, this.userAgent); if (ImmutableSet.of(HttpMethod.POST, HttpMethod.PUT, HttpMethod.DELETE) .contains(httpRequest.method())) { connection.setDoOutput(true); ByteSource.wrap(httpRequest.requestBody().orElse(ByteString.EMPTY).toByteArray()) .copyTo(connection.getOutputStream()); } int responseCode = connection.getResponseCode(); HttpHeaders.Builder responseHeadersBuilder = HttpHeaders.builder(); for (Map.Entry> headerEntry : connection.getHeaderFields().entrySet()) { String headerName = headerEntry.getKey(); if (!isNullOrEmpty(headerName)) { for (String headerValue : headerEntry.getValue()) { if (!isNullOrEmpty(headerValue)) { responseHeadersBuilder.addHeader(headerName, headerValue); } } } } return HttpResponse.builder() .setStatus(HttpStatus.fromCode(responseCode)) .setHeaders(responseHeadersBuilder.build()) .setBodyBytes(ByteString.readFrom(connection.getInputStream())) .build(); } /** * Sends the given HTTP request using this client, blocking until full response is received. * * @param httpRequest the HTTP request to be sent by this client. * @return the response returned from the HTTP server. * @throws IOException if an I/O error occurs during the HTTP request. */ @Override public HttpResponse send(HttpRequest httpRequest) throws IOException { return send(httpRequest, null); } /** * Sends the given HTTP request using this client blocking until full response is received. If * {@code networkService} is not null, the host header is set according to the service's header * field even if it resolves to a different ip. * * @param httpRequest the HTTP request to be sent by this client. * @param networkService the {@link NetworkService} proto to be used for the HOST header. * @return the response returned from the HTTP server. * @throws IOException if an I/O error occurs during the HTTP request. */ @Override public HttpResponse send(HttpRequest httpRequest, @Nullable NetworkService networkService) throws IOException { logger.atInfo().log( "%sSending HTTP '%s' request to '%s'.", logId, httpRequest.method(), httpRequest.url()); OkHttpClient callHttpClient = clientWithHostnameAsProxy(networkService); try (Response okHttpResponse = callHttpClient.newCall(buildOkHttpRequest(httpRequest, this.userAgent)).execute()) { return parseResponse(okHttpResponse); } } /** * Sends the given HTTP request using this client asynchronously. * * @param httpRequest the HTTP request to be sent by this client. * @return the future for the response to be returned from the HTTP server. */ @Override public ListenableFuture sendAsync(HttpRequest httpRequest) { return sendAsync(httpRequest, null); } /** * Sends the given HTTP request using this client asynchronously. If {@code networkService} is not * null, the host header is set according to the service's header field even if it resolves to a * different ip. * * @param httpRequest the HTTP request to be sent by this client. * @param networkService the {@link NetworkService} proto to be used for the HOST header. * @return the future for the response to be returned from the HTTP server. */ @Override public ListenableFuture sendAsync( HttpRequest httpRequest, @Nullable NetworkService networkService) { logger.atInfo().log( "%sSending async HTTP '%s' request to '%s'.", logId, httpRequest.method(), httpRequest.url()); OkHttpClient callHttpClient = clientWithHostnameAsProxy(networkService); SettableFuture responseFuture = SettableFuture.create(); Call requestCall = callHttpClient.newCall(buildOkHttpRequest(httpRequest, this.userAgent)); try { requestCall.enqueue( new Callback() { @Override public void onFailure(Call call, IOException e) { responseFuture.setException(e); } @Override public void onResponse(Call call, Response response) { try (ResponseBody unused = response.body()) { responseFuture.set(parseResponse(response)); } catch (Throwable t) { responseFuture.setException(t); } } }); } catch (Throwable t) { responseFuture.setException(t); } // Makes sure cancellation state is propagated to OkHttp. responseFuture.addListener( () -> { if (responseFuture.isCancelled()) { requestCall.cancel(); } }, directExecutor()); return responseFuture; } /* * Returns a modified HTTP client that's configured to connect to the {@code networkService}'s IP * and use its hostname in the host header, when both a hostname and an IP address is specified. * Returns an unmodified HTTP client otherwise. */ private OkHttpClient clientWithHostnameAsProxy(NetworkService networkService) { if (networkService == null) { return this.okHttpClient; } String serviceIp = networkService.getNetworkEndpoint().getIpAddress().getAddress(); String serviceHostname = networkService.getNetworkEndpoint().getHostname().getName(); return this.okHttpClient .newBuilder() .dns( hostname -> { if (hostname.equals(serviceHostname)) { hostname = serviceIp; } return Dns.SYSTEM.lookup(hostname); }) .hostnameVerifier( (hostname, session) -> { if (trustAllCertificates) { return true; } if (hostname.equals(serviceHostname)) { return true; } return HttpsURLConnection.getDefaultHostnameVerifier().verify(hostname, session); }) .build(); } private static Request buildOkHttpRequest(HttpRequest httpRequest, String userAgent) { Request.Builder okRequestBuilder = new Request.Builder().url(httpRequest.url()); httpRequest.headers().names().stream() .filter(headerName -> !Ascii.equalsIgnoreCase(headerName, USER_AGENT)) .forEach( headerName -> httpRequest .headers() .getAll(headerName) .forEach(headerValue -> okRequestBuilder.addHeader(headerName, headerValue))); okRequestBuilder.addHeader(USER_AGENT, userAgent); switch (httpRequest.method()) { case GET: okRequestBuilder.get(); break; case HEAD: okRequestBuilder.head(); break; case PUT: okRequestBuilder.put(buildRequestBody(httpRequest)); break; case POST: okRequestBuilder.post(buildRequestBody(httpRequest)); break; case DELETE: okRequestBuilder.delete(buildRequestBody(httpRequest)); break; } return okRequestBuilder.build(); } private static RequestBody buildRequestBody(HttpRequest httpRequest) { MediaType mediaType = MediaType.parse( httpRequest.headers().get(com.google.common.net.HttpHeaders.CONTENT_TYPE).orElse("")); return RequestBody.create( mediaType, httpRequest.requestBody().orElse(ByteString.EMPTY).toByteArray()); } private static HttpResponse parseResponse(Response okResponse) throws IOException { logger.atInfo().log( "Received HTTP response with code '%d' for request to '%s'.", okResponse.code(), okResponse.request().url()); HttpResponse.Builder httpResponseBuilder = HttpResponse.builder() .setStatus(HttpStatus.fromCode(okResponse.code())) .setHeaders(convertHeaders(okResponse.headers())) .setResponseUrl(okResponse.request().url()); if (!okResponse.request().method().equals(HttpMethod.HEAD.name()) && okResponse.body() != null) { httpResponseBuilder.setBodyBytes(ByteString.copyFrom(okResponse.body().bytes())); } return httpResponseBuilder.build(); } private static HttpHeaders convertHeaders(Headers headers) { HttpHeaders.Builder headersBuilder = HttpHeaders.builder(); for (int i = 0; i < headers.size(); i++) { headersBuilder.addHeader(headers.name(i), headers.value(i)); } return headersBuilder.build(); } /** * Returns a {@link Builder} that allows client code to modify the configurations of the internal * http client. * * @return the {@link Builder} for modifying this client instance. */ @Override @SuppressWarnings("unchecked") // safe covariant cast public Builder modify() { return new OkHttpHttpClientBuilder(this); } /** Builder for {@link OkHttpHttpClient}. */ // TODO(b/145315535): add more configurable options into the builder. public static class OkHttpHttpClientBuilder extends Builder { private final OkHttpClient okHttpClient; private boolean followRedirects; private boolean trustAllCertificates; private final ConnectionFactory connectionFactory; private String logId; private Duration connectionTimeout; private String userAgent; private OkHttpHttpClientBuilder(OkHttpHttpClient okHttpHttpClient) { this.okHttpClient = okHttpHttpClient.okHttpClient; this.followRedirects = okHttpClient.followRedirects(); this.trustAllCertificates = okHttpHttpClient.trustAllCertificates; this.connectionFactory = okHttpHttpClient.connectionFactory; this.logId = okHttpHttpClient.logId; this.connectionTimeout = okHttpHttpClient.connectionTimeout; this.userAgent = okHttpHttpClient.userAgent; } @Override public OkHttpHttpClientBuilder setFollowRedirects(boolean followRedirects) { this.followRedirects = followRedirects; return this; } @Override public OkHttpHttpClientBuilder setLogId(String logId) { this.logId = logId; return this; } @Override public OkHttpHttpClientBuilder setConnectTimeout(Duration connectionTimeout) { this.connectionTimeout = connectionTimeout; return this; } @CanIgnoreReturnValue public OkHttpHttpClientBuilder setUserAgent(String userAgent) { this.userAgent = userAgent; return this; } @Override public OkHttpHttpClient build() { return new OkHttpHttpClient( okHttpClient.newBuilder().followRedirects(followRedirects).build(), trustAllCertificates, connectionFactory, logId, connectionTimeout, userAgent); } } } ================================================ FILE: common/src/main/java/com/google/tsunami/common/net/http/javanet/ConnectionFactory.java ================================================ /* * Copyright 2021 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.net.http.javanet; import java.io.IOException; import java.net.HttpURLConnection; /** Given an URL, produces an {@link HttpURLConnection}. */ public interface ConnectionFactory { /** * Creates a new {@link HttpURLConnection} from the given {@code url}. * * @param url the URL to which the connection will be made * @return the created connection object, which will still be in the pre-connected state * @throws IOException if there was a problem producing the connection */ HttpURLConnection openConnection(String url) throws IOException; } ================================================ FILE: common/src/main/java/com/google/tsunami/common/net/http/javanet/DefaultConnectionFactory.java ================================================ /* * Copyright 2021 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.net.http.javanet; import static com.google.common.base.Preconditions.checkNotNull; import java.io.IOException; import java.net.HttpURLConnection; import java.net.URL; import java.time.Duration; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLSocketFactory; /** * Default implementation of {@link ConnectionFactory}, which simply attempts to open the connection * and optionally allows trusting all certifications. */ public class DefaultConnectionFactory implements ConnectionFactory { private final boolean trustAllCertificates; private final SSLSocketFactory trustAllCertsSocketFactory; private final Duration connectTimeout; private final Duration readTimeout; public DefaultConnectionFactory( boolean trustAllCertificates, SSLSocketFactory trustAllCertsSocketFactory, Duration connectTimeout, Duration readTimeout) { this.trustAllCertificates = trustAllCertificates; this.trustAllCertsSocketFactory = checkNotNull(trustAllCertsSocketFactory); this.connectTimeout = checkNotNull(connectTimeout); this.readTimeout = checkNotNull(readTimeout); } @Override public HttpURLConnection openConnection(String url) throws IOException { HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection(); connection.setConnectTimeout((int) connectTimeout.toMillis()); connection.setReadTimeout((int) readTimeout.toMillis()); if (connection instanceof HttpsURLConnection && trustAllCertificates) { HttpsURLConnection secureConnection = (HttpsURLConnection) connection; secureConnection.setSSLSocketFactory(trustAllCertsSocketFactory); secureConnection.setHostnameVerifier((hostname, session) -> true); } return connection; } } ================================================ FILE: common/src/main/java/com/google/tsunami/common/net/socket/DefaultTsunamiSocketFactory.java ================================================ /* * Copyright 2026 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.net.socket; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import com.google.common.flogger.GoogleLogger; import java.io.IOException; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.Socket; import java.time.Duration; import javax.net.SocketFactory; import javax.net.ssl.SSLSocket; import javax.net.ssl.SSLSocketFactory; /** * Default implementation of {@link TsunamiSocketFactory} that creates TCP sockets. * *

This implementation wraps the standard Java {@link SocketFactory} and {@link SSLSocketFactory} * to ensure that all created sockets have proper timeout settings configured, preventing plugins * from hanging indefinitely. */ public final class DefaultTsunamiSocketFactory implements TsunamiSocketFactory { private static final GoogleLogger logger = GoogleLogger.forEnclosingClass(); private final SocketFactory socketFactory; private final SSLSocketFactory sslSocketFactory; private final Duration defaultConnectTimeout; private final Duration defaultReadTimeout; /** * Creates a new DefaultTsunamiSocketFactory. * * @param socketFactory the underlying socket factory to use for plain TCP connections * @param sslSocketFactory the underlying SSL socket factory to use for SSL/TLS connections * @param defaultConnectTimeout the default timeout for establishing connections * @param defaultReadTimeout the default timeout for read operations */ public DefaultTsunamiSocketFactory( SocketFactory socketFactory, SSLSocketFactory sslSocketFactory, Duration defaultConnectTimeout, Duration defaultReadTimeout) { this.socketFactory = checkNotNull(socketFactory); this.sslSocketFactory = checkNotNull(sslSocketFactory); this.defaultConnectTimeout = checkNotNull(defaultConnectTimeout); this.defaultReadTimeout = checkNotNull(defaultReadTimeout); checkArgument(!defaultConnectTimeout.isNegative(), "Connect timeout cannot be negative"); checkArgument(!defaultReadTimeout.isNegative(), "Read timeout cannot be negative"); logger.atInfo().log( "TsunamiSocketFactory initialized with connect timeout: %s, read timeout: %s", defaultConnectTimeout, defaultReadTimeout); } @Override public Socket createSocket(String host, int port) throws IOException { return createSocket(host, port, defaultConnectTimeout, defaultReadTimeout); } @Override public Socket createSocket(String host, int port, Duration timeout) throws IOException { return createSocket(host, port, timeout, timeout); } @Override public Socket createSocket(String host, int port, Duration connectTimeout, Duration readTimeout) throws IOException { checkNotNull(host); checkArgument(port > 0 && port <= 65535, "Port must be between 1 and 65535"); checkNotNull(connectTimeout); checkNotNull(readTimeout); logger.atFine().log( "Creating socket to %s:%d with connect timeout %s, read timeout %s", host, port, connectTimeout, readTimeout); Socket socket = socketFactory.createSocket(); configureAndConnect(socket, new InetSocketAddress(host, port), connectTimeout, readTimeout); return socket; } @Override public Socket createSocket(InetAddress address, int port) throws IOException { return createSocket(address, port, defaultConnectTimeout, defaultReadTimeout); } @Override public Socket createSocket(InetAddress address, int port, Duration timeout) throws IOException { return createSocket(address, port, timeout, timeout); } @Override public Socket createSocket( InetAddress address, int port, Duration connectTimeout, Duration readTimeout) throws IOException { checkNotNull(address); checkArgument(port > 0 && port <= 65535, "Port must be between 1 and 65535"); checkNotNull(connectTimeout); checkNotNull(readTimeout); logger.atFine().log( "Creating socket to %s:%d with connect timeout %s, read timeout %s", address.getHostAddress(), port, connectTimeout, readTimeout); Socket socket = socketFactory.createSocket(); configureAndConnect(socket, new InetSocketAddress(address, port), connectTimeout, readTimeout); return socket; } @Override public Socket createUnconnectedSocket() throws IOException { Socket socket = socketFactory.createSocket(); socket.setSoTimeout((int) defaultReadTimeout.toMillis()); logger.atFine().log("Created unconnected socket with read timeout %s", defaultReadTimeout); return socket; } @Override public SSLSocket createSslSocket(String host, int port) throws IOException { return createSslSocket(host, port, defaultConnectTimeout, defaultReadTimeout); } @Override public SSLSocket createSslSocket(String host, int port, Duration timeout) throws IOException { return createSslSocket(host, port, timeout, timeout); } @Override public SSLSocket createSslSocket( String host, int port, Duration connectTimeout, Duration readTimeout) throws IOException { checkNotNull(host); checkArgument(port > 0 && port <= 65535, "Port must be between 1 and 65535"); checkNotNull(connectTimeout); checkNotNull(readTimeout); logger.atFine().log( "Creating SSL socket to %s:%d with connect timeout %s, read timeout %s", host, port, connectTimeout, readTimeout); // Create a plain socket first, connect with timeout, then wrap with SSL Socket plainSocket = socketFactory.createSocket(); configureAndConnect( plainSocket, new InetSocketAddress(host, port), connectTimeout, readTimeout); SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(plainSocket, host, port, true); sslSocket.setSoTimeout((int) readTimeout.toMillis()); sslSocket.startHandshake(); return sslSocket; } @Override public SSLSocket createSslSocket(InetAddress address, int port) throws IOException { return createSslSocket(address, port, defaultConnectTimeout, defaultReadTimeout); } @Override public SSLSocket createSslSocket(InetAddress address, int port, Duration timeout) throws IOException { return createSslSocket(address, port, timeout, timeout); } @Override public SSLSocket createSslSocket( InetAddress address, int port, Duration connectTimeout, Duration readTimeout) throws IOException { checkNotNull(address); checkArgument(port > 0 && port <= 65535, "Port must be between 1 and 65535"); checkNotNull(connectTimeout); checkNotNull(readTimeout); logger.atFine().log( "Creating SSL socket to %s:%d with connect timeout %s, read timeout %s", address.getHostAddress(), port, connectTimeout, readTimeout); // Create a plain socket first, connect with timeout, then wrap with SSL Socket plainSocket = socketFactory.createSocket(); configureAndConnect( plainSocket, new InetSocketAddress(address, port), connectTimeout, readTimeout); SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(plainSocket, address.getHostAddress(), port, true); sslSocket.setSoTimeout((int) readTimeout.toMillis()); sslSocket.startHandshake(); return sslSocket; } @Override public SSLSocket wrapWithSsl(Socket socket, String host, int port, boolean autoClose) throws IOException { checkNotNull(socket); checkNotNull(host); checkArgument(port > 0 && port <= 65535, "Port must be between 1 and 65535"); logger.atFine().log("Wrapping existing socket with SSL for host %s:%d", host, port); SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(socket, host, port, autoClose); // Preserve the timeout from the original socket if set, otherwise use default int originalTimeout = socket.getSoTimeout(); if (originalTimeout > 0) { sslSocket.setSoTimeout(originalTimeout); } else { sslSocket.setSoTimeout((int) defaultReadTimeout.toMillis()); } sslSocket.startHandshake(); return sslSocket; } @Override public Duration getDefaultConnectTimeout() { return defaultConnectTimeout; } @Override public Duration getDefaultReadTimeout() { return defaultReadTimeout; } /** * Configures socket options and connects to the specified address with timeout. * * @param socket the socket to configure and connect * @param address the address to connect to * @param connectTimeout the timeout for establishing the connection * @param readTimeout the timeout for read operations * @throws IOException if an I/O error occurs */ private void configureAndConnect( Socket socket, InetSocketAddress address, Duration connectTimeout, Duration readTimeout) throws IOException { // Set read timeout before connecting socket.setSoTimeout((int) readTimeout.toMillis()); // Enable TCP keep-alive to detect dead connections socket.setKeepAlive(true); // Disable Nagle's algorithm for better latency in security scanning socket.setTcpNoDelay(true); // Connect with timeout socket.connect(address, (int) connectTimeout.toMillis()); logger.atFine().log( "Socket connected to %s with SO_TIMEOUT=%dms", address, readTimeout.toMillis()); } } ================================================ FILE: common/src/main/java/com/google/tsunami/common/net/socket/TsunamiSocketFactory.java ================================================ /* * Copyright 2026 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.net.socket; import java.io.IOException; import java.net.InetAddress; import java.net.Socket; import java.time.Duration; import javax.net.ssl.SSLSocket; /** * A socket factory API for creating TCP sockets with enforced default configurations. * *

This API provides a normalized way to create sockets from Tsunami plugins, ensuring that * proper timeouts are always configured. This prevents plugins from hanging indefinitely when * servers don't respond. * *

Plugins should use this factory instead of directly creating sockets through {@link * javax.net.SocketFactory} or {@link javax.net.ssl.SSLSocketFactory} to ensure consistent behavior * and proper timeout handling. * *

Example usage: * *

{@code
 * @Inject
 * TsunamiSocketFactory socketFactory;
 *
 * // Create a socket with default timeouts
 * Socket socket = socketFactory.createSocket("example.com", 80);
 *
 * // Create a socket with custom timeouts
 * Socket socket = socketFactory.createSocket("example.com", 80,
 *     Duration.ofSeconds(5), Duration.ofSeconds(10));
 *
 * // Create an SSL socket
 * SSLSocket sslSocket = socketFactory.createSslSocket("example.com", 443);
 * }
*/ public interface TsunamiSocketFactory { /** * Creates a TCP socket connected to the specified host and port with default timeouts. * *

The socket will be configured with the default connect and read timeouts specified in the * Tsunami configuration. If no configuration is provided, sensible defaults will be used. * * @param host the host to connect to * @param port the port to connect to * @return a connected socket with enforced timeouts * @throws IOException if an I/O error occurs while creating the socket */ Socket createSocket(String host, int port) throws IOException; /** * Creates a TCP socket connected to the specified host and port with a single timeout. * *

The specified timeout will be used for both connection establishment and read operations. * * @param host the host to connect to * @param port the port to connect to * @param timeout the timeout for both connection and read operations * @return a connected socket with the specified timeout * @throws IOException if an I/O error occurs while creating the socket */ Socket createSocket(String host, int port, Duration timeout) throws IOException; /** * Creates a TCP socket connected to the specified host and port with custom timeouts. * * @param host the host to connect to * @param port the port to connect to * @param connectTimeout the timeout for establishing the connection * @param readTimeout the timeout for read operations (SO_TIMEOUT) * @return a connected socket with the specified timeouts * @throws IOException if an I/O error occurs while creating the socket */ Socket createSocket(String host, int port, Duration connectTimeout, Duration readTimeout) throws IOException; /** * Creates a TCP socket connected to the specified address and port with default timeouts. * * @param address the IP address to connect to * @param port the port to connect to * @return a connected socket with enforced timeouts * @throws IOException if an I/O error occurs while creating the socket */ Socket createSocket(InetAddress address, int port) throws IOException; /** * Creates a TCP socket connected to the specified address and port with a single timeout. * *

The specified timeout will be used for both connection establishment and read operations. * * @param address the IP address to connect to * @param port the port to connect to * @param timeout the timeout for both connection and read operations * @return a connected socket with the specified timeout * @throws IOException if an I/O error occurs while creating the socket */ Socket createSocket(InetAddress address, int port, Duration timeout) throws IOException; /** * Creates a TCP socket connected to the specified address and port with custom timeouts. * * @param address the IP address to connect to * @param port the port to connect to * @param connectTimeout the timeout for establishing the connection * @param readTimeout the timeout for read operations (SO_TIMEOUT) * @return a connected socket with the specified timeouts * @throws IOException if an I/O error occurs while creating the socket */ Socket createSocket(InetAddress address, int port, Duration connectTimeout, Duration readTimeout) throws IOException; /** * Creates an unconnected TCP socket with default timeouts configured. * *

The returned socket will have SO_TIMEOUT set to the default read timeout. The caller is * responsible for connecting the socket, which should be done with a timeout. * * @return an unconnected socket with default read timeout configured * @throws IOException if an I/O error occurs while creating the socket */ Socket createUnconnectedSocket() throws IOException; /** * Creates an SSL/TLS socket connected to the specified host and port with default timeouts. * *

The socket will be configured with the default connect and read timeouts. SSL/TLS handshake * will be performed automatically. * * @param host the host to connect to * @param port the port to connect to * @return a connected SSL socket with enforced timeouts * @throws IOException if an I/O error occurs while creating the socket */ SSLSocket createSslSocket(String host, int port) throws IOException; /** * Creates an SSL/TLS socket connected to the specified host and port with a single timeout. * *

The specified timeout will be used for both connection establishment and read operations. * * @param host the host to connect to * @param port the port to connect to * @param timeout the timeout for both connection and read operations * @return a connected SSL socket with the specified timeout * @throws IOException if an I/O error occurs while creating the socket */ SSLSocket createSslSocket(String host, int port, Duration timeout) throws IOException; /** * Creates an SSL/TLS socket connected to the specified host and port with custom timeouts. * * @param host the host to connect to * @param port the port to connect to * @param connectTimeout the timeout for establishing the connection * @param readTimeout the timeout for read operations (SO_TIMEOUT) * @return a connected SSL socket with the specified timeouts * @throws IOException if an I/O error occurs while creating the socket */ SSLSocket createSslSocket(String host, int port, Duration connectTimeout, Duration readTimeout) throws IOException; /** * Creates an SSL/TLS socket connected to the specified address and port with default timeouts. * * @param address the IP address to connect to * @param port the port to connect to * @return a connected SSL socket with enforced timeouts * @throws IOException if an I/O error occurs while creating the socket */ SSLSocket createSslSocket(InetAddress address, int port) throws IOException; /** * Creates an SSL/TLS socket connected to the specified address and port with a single timeout. * *

The specified timeout will be used for both connection establishment and read operations. * * @param address the IP address to connect to * @param port the port to connect to * @param timeout the timeout for both connection and read operations * @return a connected SSL socket with the specified timeout * @throws IOException if an I/O error occurs while creating the socket */ SSLSocket createSslSocket(InetAddress address, int port, Duration timeout) throws IOException; /** * Creates an SSL/TLS socket connected to the specified address and port with custom timeouts. * * @param address the IP address to connect to * @param port the port to connect to * @param connectTimeout the timeout for establishing the connection * @param readTimeout the timeout for read operations (SO_TIMEOUT) * @return a connected SSL socket with the specified timeouts * @throws IOException if an I/O error occurs while creating the socket */ SSLSocket createSslSocket( InetAddress address, int port, Duration connectTimeout, Duration readTimeout) throws IOException; /** * Wraps an existing socket with SSL/TLS. * *

This method is useful when you need to upgrade a plain TCP connection to SSL/TLS, such as * when implementing STARTTLS protocols. * * @param socket the existing socket to wrap * @param host the hostname for SNI (Server Name Indication) * @param port the port number * @param autoClose whether the underlying socket should be closed when the SSL socket is closed * @return an SSL socket wrapping the existing socket * @throws IOException if an I/O error occurs while creating the SSL socket */ SSLSocket wrapWithSsl(Socket socket, String host, int port, boolean autoClose) throws IOException; /** * Returns the default connect timeout configured for this factory. * * @return the default connect timeout */ Duration getDefaultConnectTimeout(); /** * Returns the default read timeout configured for this factory. * * @return the default read timeout */ Duration getDefaultReadTimeout(); } ================================================ FILE: common/src/main/java/com/google/tsunami/common/net/socket/TsunamiSocketFactoryCliOptions.java ================================================ /* * Copyright 2025 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.net.socket; import com.beust.jcommander.Parameter; import com.beust.jcommander.ParameterException; import com.beust.jcommander.Parameters; import com.google.tsunami.common.cli.CliOption; import org.checkerframework.checker.nullness.qual.Nullable; /** * Command line arguments for {@link TsunamiSocketFactory}. * *

These options allow users to override socket timeout settings from the command line. CLI * options take precedence over configuration file settings. */ @Parameters(separators = "=") public final class TsunamiSocketFactoryCliOptions implements CliOption { @Parameter( names = "--socket-connect-timeout-seconds", description = "The timeout in seconds for establishing TCP connections. This timeout applies to the" + " socket connect operation. Default is 10 seconds.") public Integer connectTimeoutSeconds; @Parameter( names = "--socket-read-timeout-seconds", description = "The timeout in seconds for read operations on sockets (SO_TIMEOUT). If no data is" + " received within this time, a SocketTimeoutException will be thrown. Default is 30" + " seconds.") public Integer readTimeoutSeconds; @Parameter( names = "--socket-trust-all-certificates", arity = 1, description = "Whether SSL/TLS connections should trust all certificates. When true, accepts any SSL" + " certificate without validation. Useful for scanning targets with self-signed" + " certificates. Default is true.") public Boolean trustAllCertificates; @Parameter( names = "--socket-disable-timeouts", arity = 1, description = "Disable all socket timeouts, allowing connections to wait indefinitely. WARNING: This" + " can cause plugins to hang forever if servers do not respond. Use with caution." + " Default is false.") public Boolean disableTimeouts; @Override public void validate() { // Skip timeout validation if timeouts are disabled if (disableTimeouts != null && disableTimeouts) { return; } validateTimeout("--socket-connect-timeout-seconds", connectTimeoutSeconds); validateTimeout("--socket-read-timeout-seconds", readTimeoutSeconds); } private static void validateTimeout(String flagName, @Nullable Integer value) { if (value != null && value < 0) { throw new ParameterException( String.format("%s cannot be a negative number, received %d.", flagName, value)); } if (value != null && value == 0) { throw new ParameterException( String.format( "%s cannot be zero. Use a positive value for timeouts or pass the" + " --socket-disable-timeouts flag to disable timeouts entirely. Received %d.", flagName, value)); } } } ================================================ FILE: common/src/main/java/com/google/tsunami/common/net/socket/TsunamiSocketFactoryConfigProperties.java ================================================ /* * Copyright 2025 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.net.socket; import com.google.tsunami.common.config.annotations.ConfigProperties; /** * Configuration properties for {@link TsunamiSocketFactory}. * *

These properties can be set in the Tsunami configuration file (e.g., tsunami.yaml) under the * {@code common.net.socket} prefix: * *

{@code
 * common:
 *   net:
 *     socket:
 *       connect_timeout_seconds: 10
 *       read_timeout_seconds: 30
 *       trust_all_certificates: true
 *       disable_timeouts: false
 * }
*/ @ConfigProperties("common.net.socket") public final class TsunamiSocketFactoryConfigProperties { /** * The timeout in seconds for establishing TCP connections. * *

This timeout applies to the socket connect operation. If the connection cannot be * established within this time, a {@link java.net.SocketTimeoutException} will be thrown. * *

Default value is 10 seconds if not specified. */ Integer connectTimeoutSeconds; /** * The timeout in seconds for read operations on sockets. * *

This timeout is set as the socket's SO_TIMEOUT option. If no data is received within this * time, a {@link java.net.SocketTimeoutException} will be thrown. * *

Default value is 30 seconds if not specified. This is intentionally longer than the connect * timeout to allow for slow servers or large data transfers. */ Integer readTimeoutSeconds; /** * Whether SSL/TLS connections should trust all certificates. * *

When set to true, the socket factory will accept any SSL certificate without validation. * This is useful for security scanning where targets may have self-signed or expired * certificates. * *

Warning: This should only be used in controlled environments. Setting this * to true disables certificate validation which could expose the scanner to man-in-the-middle * attacks. * *

Default value is true for security scanning purposes. */ Boolean trustAllCertificates; /** * Whether to disable all socket timeouts. * *

When set to true, sockets will wait indefinitely for connections and data. This means * plugins may hang forever if a server does not respond. * *

Warning: Disabling timeouts is dangerous and can cause the scanner to hang * indefinitely. Only use this option if you have a specific need to wait for slow or unresponsive * servers. * *

Default value is false (timeouts are enforced). */ Boolean disableTimeouts; } ================================================ FILE: common/src/main/java/com/google/tsunami/common/net/socket/TsunamiSocketFactoryModule.java ================================================ /* * Copyright 2025 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.net.socket; import com.google.inject.AbstractModule; import com.google.inject.Provides; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.security.GeneralSecurityException; import java.security.SecureRandom; import java.security.cert.X509Certificate; import java.time.Duration; import javax.inject.Qualifier; import javax.inject.Singleton; import javax.net.SocketFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; /** * Guice module for installing {@link TsunamiSocketFactory}. * *

This module provides a {@link TsunamiSocketFactory} instance that creates sockets with * enforced timeout configurations. It integrates with Tsunami's configuration system to allow * customization of default timeouts through both configuration files and command-line options. * *

Example usage in a plugin: * *

{@code
 * public class MyPlugin implements VulnDetector {
 *   private final TsunamiSocketFactory socketFactory;
 *
 *   @Inject
 *   MyPlugin(TsunamiSocketFactory socketFactory) {
 *     this.socketFactory = socketFactory;
 *   }
 *
 *   public void doSomething() throws IOException {
 *     // Socket will have enforced timeouts
 *     Socket socket = socketFactory.createSocket("example.com", 80);
 *     // ...
 *   }
 * }
 * }
*/ public final class TsunamiSocketFactoryModule extends AbstractModule { // Default timeout values private static final int DEFAULT_CONNECT_TIMEOUT_SECONDS = 10; private static final int DEFAULT_READ_TIMEOUT_SECONDS = 30; private static final boolean DEFAULT_TRUST_ALL_CERTIFICATES = true; private static final boolean DEFAULT_DISABLE_TIMEOUTS = false; // This TrustManager does NOT validate certificate chains. private static final X509TrustManager TRUST_ALL_CERTS_MANAGER = new X509TrustManager() { @Override public void checkClientTrusted(X509Certificate[] chain, String authType) {} @Override public void checkServerTrusted(X509Certificate[] chain, String authType) {} @Override public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; } }; @Override protected void configure() { // Module configuration is handled by @Provides methods } @Provides @Singleton TsunamiSocketFactory provideTsunamiSocketFactory( @TrustAllCertificates boolean trustAllCertificates, @DisableTimeouts boolean disableTimeouts, @ConnectTimeoutSeconds int connectTimeoutSeconds, @ReadTimeoutSeconds int readTimeoutSeconds) throws GeneralSecurityException { SocketFactory socketFactory = SocketFactory.getDefault(); SSLSocketFactory sslSocketFactory; if (trustAllCertificates) { SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(null, new TrustManager[] {TRUST_ALL_CERTS_MANAGER}, new SecureRandom()); sslSocketFactory = sslContext.getSocketFactory(); } else { sslSocketFactory = (SSLSocketFactory) SSLSocketFactory.getDefault(); } // When timeouts are disabled, use 0 which means infinite timeout in Java Duration connectTimeout = disableTimeouts ? Duration.ZERO : Duration.ofSeconds(connectTimeoutSeconds); Duration readTimeout = disableTimeouts ? Duration.ZERO : Duration.ofSeconds(readTimeoutSeconds); return new DefaultTsunamiSocketFactory( socketFactory, sslSocketFactory, connectTimeout, readTimeout); } @Provides @DisableTimeouts boolean provideDisableTimeouts( TsunamiSocketFactoryCliOptions cliOptions, TsunamiSocketFactoryConfigProperties configProperties) { if (cliOptions.disableTimeouts != null) { return cliOptions.disableTimeouts; } if (configProperties.disableTimeouts != null) { return configProperties.disableTimeouts; } return DEFAULT_DISABLE_TIMEOUTS; } @Provides @TrustAllCertificates boolean provideTrustAllCertificates( TsunamiSocketFactoryCliOptions cliOptions, TsunamiSocketFactoryConfigProperties configProperties) { if (cliOptions.trustAllCertificates != null) { return cliOptions.trustAllCertificates; } if (configProperties.trustAllCertificates != null) { return configProperties.trustAllCertificates; } return DEFAULT_TRUST_ALL_CERTIFICATES; } @Provides @ConnectTimeoutSeconds int provideConnectTimeoutSeconds( TsunamiSocketFactoryCliOptions cliOptions, TsunamiSocketFactoryConfigProperties configProperties) { if (cliOptions.connectTimeoutSeconds != null) { return cliOptions.connectTimeoutSeconds; } if (configProperties.connectTimeoutSeconds != null) { return configProperties.connectTimeoutSeconds; } return DEFAULT_CONNECT_TIMEOUT_SECONDS; } @Provides @ReadTimeoutSeconds int provideReadTimeoutSeconds( TsunamiSocketFactoryCliOptions cliOptions, TsunamiSocketFactoryConfigProperties configProperties) { if (cliOptions.readTimeoutSeconds != null) { return cliOptions.readTimeoutSeconds; } if (configProperties.readTimeoutSeconds != null) { return configProperties.readTimeoutSeconds; } return DEFAULT_READ_TIMEOUT_SECONDS; } /** Qualifier for whether to disable all socket timeouts. */ @Qualifier @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD}) public @interface DisableTimeouts {} /** Qualifier for whether to trust all SSL certificates. */ @Qualifier @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD}) public @interface TrustAllCertificates {} /** Qualifier for the connect timeout in seconds. */ @Qualifier @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD}) public @interface ConnectTimeoutSeconds {} /** Qualifier for the read timeout in seconds. */ @Qualifier @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD}) public @interface ReadTimeoutSeconds {} } ================================================ FILE: common/src/main/java/com/google/tsunami/common/reflection/ClassGraphModule.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.reflection; import static com.google.common.base.Preconditions.checkNotNull; import com.google.inject.AbstractModule; import io.github.classgraph.ScanResult; /** Guice module for providing ClassGraph bindings. */ public final class ClassGraphModule extends AbstractModule { private final ScanResult scanResult; public ClassGraphModule(ScanResult scanResult) { this.scanResult = checkNotNull(scanResult); } @Override protected void configure() { bind(ScanResult.class).annotatedWith(RuntimeClassGraphScanResult.class).toInstance(scanResult); } } ================================================ FILE: common/src/main/java/com/google/tsunami/common/reflection/RuntimeClassGraphScanResult.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.reflection; import static java.lang.annotation.ElementType.FIELD; import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.ElementType.PARAMETER; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import javax.inject.Qualifier; /** Annotation for the ClassGraph {@link io.github.classgraph.ScanResult} at runtime. */ @Qualifier @Retention(RetentionPolicy.RUNTIME) @Target({PARAMETER, METHOD, FIELD}) public @interface RuntimeClassGraphScanResult {} ================================================ FILE: common/src/main/java/com/google/tsunami/common/server/CompactRunRequestHelper.java ================================================ /* * Copyright 2024 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.server; import com.google.common.collect.ImmutableList; import com.google.tsunami.proto.MatchedPlugin; import com.google.tsunami.proto.NetworkService; import com.google.tsunami.proto.RunCompactRequest; import com.google.tsunami.proto.RunCompactRequest.PluginNetworkServiceTarget; import com.google.tsunami.proto.RunRequest; import java.util.HashMap; /** * CompactRunRequestHelper is a helper class to compress/uncompress the RunRequest into/from the * compact representation. */ public final class CompactRunRequestHelper { private CompactRunRequestHelper() {} public static RunCompactRequest compress(RunRequest runRequest) { var builder = RunCompactRequest.newBuilder().setTarget(runRequest.getTarget()); HashMap serviceIndexMap = new HashMap<>(); int pluginIndex = -1; for (MatchedPlugin matchedPlugin : runRequest.getPluginsList()) { pluginIndex++; builder.addPlugins(matchedPlugin.getPlugin()); for (NetworkService service : matchedPlugin.getServicesList()) { Integer serviceIndex = serviceIndexMap.get(service); if (serviceIndex == null) { serviceIndex = serviceIndexMap.size(); serviceIndexMap.put(service, serviceIndex); builder.addServices(service); } builder.addScanTargets( PluginNetworkServiceTarget.newBuilder() .setPluginIndex(pluginIndex) .setServiceIndex(serviceIndex) .build()); } } return builder.build(); } public static RunRequest uncompress(RunCompactRequest runCompactRequest) { ImmutableList.Builder matchedPlugins = ImmutableList.builder(); for (var target : runCompactRequest.getScanTargetsList()) { var plugin = runCompactRequest.getPlugins(target.getPluginIndex()); var networkService = runCompactRequest.getServices(target.getServiceIndex()); matchedPlugins.add( MatchedPlugin.newBuilder().setPlugin(plugin).addServices(networkService).build()); } return RunRequest.newBuilder() .setTarget(runCompactRequest.getTarget()) .addAllPlugins(matchedPlugins.build()) .build(); } } ================================================ FILE: common/src/main/java/com/google/tsunami/common/server/LanguageServerCommand.java ================================================ /* * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.server; import com.google.auto.value.AutoValue; import java.time.Duration; import javax.annotation.Nullable; /** Command to spawn a language server and associated command lines. */ @AutoValue public abstract class LanguageServerCommand { public static LanguageServerCommand create( String serverCommand, @Nullable String serverAddress, String port, String logId, String outputDir, boolean trustAllSsl, Duration timeoutSeconds, String callbackAddress, int callbackPort, String pollingUri, int deadlineRunSeconds) { return new AutoValue_LanguageServerCommand( serverCommand, serverAddress, port, logId, outputDir, trustAllSsl, timeoutSeconds, callbackAddress, callbackPort, pollingUri, deadlineRunSeconds); } public abstract String serverCommand(); public abstract String serverAddress(); public abstract String port(); public abstract String logId(); public abstract String outputDir(); public abstract boolean trustAllSslCert(); public abstract Duration timeoutSeconds(); public abstract String callbackAddress(); public abstract int callbackPort(); public abstract String pollingUri(); public abstract int deadlineRunSeconds(); } ================================================ FILE: common/src/main/java/com/google/tsunami/common/time/SystemUtcClockModule.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.time; import com.google.inject.AbstractModule; import java.time.Clock; /** Binds {@link java.time.Clock} to a {@code Clock.systemUTC()}. */ public class SystemUtcClockModule extends AbstractModule { @Override protected void configure() { bind(Clock.class).annotatedWith(UtcClock.class).toInstance(Clock.systemUTC()); } } ================================================ FILE: common/src/main/java/com/google/tsunami/common/time/UtcClock.java ================================================ /* * Copyright 2019 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.time; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import javax.inject.Qualifier; /** Annotation for the UTC {@link java.time.Clock} used in Tsunami. */ @Qualifier @Retention(RetentionPolicy.RUNTIME) public @interface UtcClock {} ================================================ FILE: common/src/main/java/com/google/tsunami/common/time/testing/FakeUtcClock.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.time.testing; import static com.google.common.base.Preconditions.checkNotNull; import java.time.Clock; import java.time.Duration; import java.time.Instant; import java.time.ZoneId; import java.time.ZoneOffset; import java.util.concurrent.atomic.AtomicReference; /** * Implementation of a {@link java.time.Clock} that returns a settable {@link Instant} value. * *

By default, the clock is set to the {@link Instant} when the clock is created. Clock can be * set to a specific instant by {@link #setNow}. */ public final class FakeUtcClock extends Clock { private final AtomicReference nowReference = new AtomicReference<>(); private FakeUtcClock(Instant now) { nowReference.set(checkNotNull(now)); } /** * Create a {@link FakeUtcClock} instance initialized to UTC now. * *

To create a fake UTC clock at a specific instant, calling {@code setNow()} as in: * *

{@code FakeUtcClock fakeUtcClock = FakeUtcClock.create().setNow(TARGET_INSTANT);}
* * @return a {@link FakeUtcClock} instance. */ public static FakeUtcClock create() { return new FakeUtcClock(Instant.now()); } /** * Sets the return value of {@link #instant()}. * * @param now the instant that this clock points to * @return this */ public FakeUtcClock setNow(Instant now) { nowReference.set(checkNotNull(now)); return this; } /** * Advances the clock by the given duration. * *

NOTE: this method can be called with a negative duration if the clock needs to go back in * time. * * @param increment the duration to advance the clock by * @return this */ public FakeUtcClock advance(Duration increment) { checkNotNull(increment); nowReference.getAndUpdate(now -> now.plus(increment)); return this; } @Override public Instant instant() { return nowReference.get(); } @Override public ZoneId getZone() { return ZoneOffset.UTC; } @Override public Clock withZone(ZoneId zone) { throw new UnsupportedOperationException("Setting ZoneId to FakeUtcClock is not supported"); } @Override public boolean equals(Object obj) { if (obj instanceof FakeUtcClock) { FakeUtcClock other = (FakeUtcClock) obj; return nowReference.get().equals(other.nowReference.get()); } return false; } @Override public int hashCode() { return nowReference.get().hashCode(); } @Override public String toString() { return String.format("FakeUtcClock(now = %s)", nowReference.get()); } } ================================================ FILE: common/src/main/java/com/google/tsunami/common/time/testing/FakeUtcClockModule.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.time.testing; import static com.google.common.base.Preconditions.checkNotNull; import com.google.inject.AbstractModule; import com.google.tsunami.common.time.UtcClock; import java.time.Clock; /** Binds {@link java.time.Clock} to a {@link FakeUtcClock}. */ public class FakeUtcClockModule extends AbstractModule { private final FakeUtcClock fakeUtcClock; public FakeUtcClockModule() { this.fakeUtcClock = FakeUtcClock.create(); } public FakeUtcClockModule(FakeUtcClock fakeUtcClock) { this.fakeUtcClock = checkNotNull(fakeUtcClock); } @Override protected void configure() { bind(Clock.class).annotatedWith(UtcClock.class).toInstance(fakeUtcClock); bind(FakeUtcClock.class).annotatedWith(UtcClock.class).toInstance(fakeUtcClock); } } ================================================ FILE: common/src/main/java/com/google/tsunami/common/version/ComparisonUtility.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.version; import java.util.List; /** Utility for version related comparison. */ final class ComparisonUtility { private ComparisonUtility() {} /** * Compares two lists. If one list is shorter than the other, {@code fillValue} is used for * comparison. * *

For example, comparing {@code [1, 2, 3]} and {@code [1, 2, 3, 4]} with {@code fillValue} * equals to 10 is equivalent of comparing {@code [1, 2, 3, 10]} with {@code [1, 2, 3, 4]}. * * @param left list for comparison. * @param right list for comparison. * @param fillValue value to use if one list is shorter than the other. * @param element type of the list. * @return 0 if two lists equals, -1 if {@code left} is less than {@code right}, 1 otherwise. */ static > int compareListWithFillValue( List left, List right, T fillValue) { int longest = Math.max(left.size(), right.size()); for (int i = 0; i < longest; i++) { T leftElement = fillValue; T rightElement = fillValue; if (i < left.size()) { leftElement = left.get(i); } if (i < right.size()) { rightElement = right.get(i); } int compareResult = leftElement.compareTo(rightElement); if (compareResult != 0) { return compareResult; } } return 0; } } ================================================ FILE: common/src/main/java/com/google/tsunami/common/version/KnownQualifier.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.version; import static com.google.common.base.Preconditions.checkNotNull; import com.google.common.base.Ascii; import java.util.Arrays; /** * A list of all the known text token qualifiers from our vulnerability feeds and the order here * represents the precedence of these identifiers. */ enum KnownQualifier implements Comparable { ALPHA("alpha"), BETA("beta"), PRE("pre"), R("r"), RC("rc"), ABSENT(""), P("p"), PATCH("patch"), PATCHED("patched"); private final String qualifierText; KnownQualifier(String qualifierText) { this.qualifierText = qualifierText; } String getQualifierText() { return this.qualifierText; } static boolean isKnownQualifier(String string) { checkNotNull(string); return Arrays.stream(KnownQualifier.values()) .anyMatch(knownQualifier -> Ascii.equalsIgnoreCase(knownQualifier.qualifierText, string)); } static KnownQualifier fromText(String string) { checkNotNull(string); return Arrays.stream(KnownQualifier.values()) .filter(knownQualifier -> Ascii.equalsIgnoreCase(knownQualifier.qualifierText, string)) .findFirst() .orElseThrow( () -> new IllegalArgumentException( String.format("%s is not a valid KnownQualifier text.", string))); } } ================================================ FILE: common/src/main/java/com/google/tsunami/common/version/Segment.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.version; import com.google.auto.value.AutoValue; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.errorprone.annotations.Immutable; import java.util.Arrays; import java.util.regex.Pattern; import java.util.stream.Collectors; /** * The segment of a version number, separated by the semantic separators from the original version * string. * *

For example, there are 2 segments in version string "2.1.1-alpha.1": "2.1.1" and "alpha.1". * *

The first element of a Segment should always be a {@link KnownQualifier} token. If no {@link * KnownQualifier} is found in the segment, an ABSENT qualifier is added. */ @AutoValue @Immutable abstract class Segment implements Comparable { static final Segment NULL = Segment.fromTokenList(ImmutableList.of(Token.EMPTY)); private static final ImmutableSet TOKENIZER_DELIMITERS = ImmutableSet.of("\\.", "\\+", "-", ":", "_", "~"); private static final Pattern TOKENIZER_SPLIT_REGEX = Pattern.compile( // Additional split on boundaries between number and text. "(?<=\\D)(?=\\d)|(?<=\\d)(?=\\D)|" // We keep the delimiter for comparison. + TOKENIZER_DELIMITERS.stream() .map(delimiter -> String.format("((?<=%1$s)|(?=%1$s))", delimiter)) .collect(Collectors.joining("|"))); private static final ImmutableSet EXCLUDED_TOKENS = ImmutableSet.of(".", "gg", "N/A"); /** All the tokens within this segment. */ abstract ImmutableList tokens(); static Segment fromTokenList(ImmutableList tokens) { ImmutableList.Builder finalTokensBuilder = new ImmutableList.Builder<>(); // Make sure Segment always starts with a KnownQualifier. See http://b/135912609 for details. if (tokens.isEmpty() || !tokens.get(0).isKnownQualifier()) { finalTokensBuilder.add(Token.fromKnownQualifier(KnownQualifier.ABSENT)); } finalTokensBuilder.addAll(tokens); return new AutoValue_Segment(finalTokensBuilder.build()); } static Segment fromString(String segmentString) { return parseFromString(segmentString); } private static Segment parseFromString(String segmentString) { ImmutableList rawTokens = Arrays.stream(TOKENIZER_SPLIT_REGEX.split(segmentString)) .filter(token -> !token.isEmpty()) .filter(token -> !EXCLUDED_TOKENS.contains(token)) .collect(ImmutableList.toImmutableList()); if (rawTokens.isEmpty()) { return Segment.NULL; } ImmutableList.Builder tokensBuilder = new ImmutableList.Builder<>(); // Make sure Segment always starts with a KnownQualifier. See http://b/135912609 for details. if (!KnownQualifier.isKnownQualifier(rawTokens.get(0))) { tokensBuilder.add(Token.fromKnownQualifier(KnownQualifier.ABSENT)); } // Parses token. for (String rawToken : rawTokens) { try { long numericToken = Long.parseLong(rawToken); tokensBuilder.add(Token.fromNumeric(numericToken)); } catch (NumberFormatException e) { tokensBuilder.add(Token.fromText(rawToken)); } } return Segment.fromTokenList(tokensBuilder.build()); } @Override public int compareTo(Segment other) { return ComparisonUtility.compareListWithFillValue(this.tokens(), other.tokens(), Token.EMPTY); } } ================================================ FILE: common/src/main/java/com/google/tsunami/common/version/Token.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.version; import com.google.auto.value.AutoOneOf; import com.google.common.base.Ascii; import com.google.errorprone.annotations.Immutable; /** * Represents a token from a version string. * *

A token is the smallest meaningful piece of a version string. For example, there are 3 tokens * in version 2.1.1: [token(2), token(1), token(1)]. */ @Immutable @AutoOneOf(Token.Kind.class) abstract class Token implements Comparable { static final Token EMPTY = Token.fromKnownQualifier(KnownQualifier.ABSENT); /** All types of a token, required by the AutoOneOf annotation. */ public enum Kind { NUMERIC, TEXT } abstract Kind getKind(); abstract long getNumeric(); static Token fromNumeric(long numeric) { return AutoOneOf_Token.numeric(numeric); } boolean isNumeric() { return getKind().equals(Kind.NUMERIC); } abstract String getText(); static Token fromText(String string) { return AutoOneOf_Token.text(Ascii.toLowerCase(string)); } boolean isText() { return getKind().equals(Kind.TEXT); } static Token fromKnownQualifier(KnownQualifier knownQualifier) { return AutoOneOf_Token.text(knownQualifier.getQualifierText()); } boolean isKnownQualifier() { return isText() && KnownQualifier.isKnownQualifier(getText()); } boolean isEmptyToken() { return isText() && getText().isEmpty(); } @Override public int compareTo(Token other) { // Empty tokens are always the same. if (this.isEmptyToken() && other.isEmptyToken()) { return 0; } /* * If the tokens under comparison are one Empty token and one Numeric token, then Empty token * should always be less than Numeric token, e.g. version 2.1 is less than 2.1.1 (i.e. * 2.1.empty < 2.1.1, empty < 1). */ if (this.isEmptyToken() && other.isNumeric()) { return -1; } if (this.isNumeric() && other.isEmptyToken()) { return 1; } /* * For Numeric and Text tokens, we follow the specification from http://semver.org#spec-item-11: * 1. Numeric tokens are compared numerically. * 2. Text tokens are compared lexically, case insensitively. * 3. Numeric identifiers always have lower precedence than non-numeric identifiers. * * For all the known qualifiers, we apply the comparison rules defined by KnownQualifier. */ if (this.isNumeric() && other.isNumeric()) { return Long.compare(this.getNumeric(), other.getNumeric()); } if (this.isKnownQualifier() && other.isKnownQualifier()) { return KnownQualifier.fromText(this.getText()) .compareTo(KnownQualifier.fromText(other.getText())); } if (this.isText() && other.isText()) { return this.getText().compareToIgnoreCase(other.getText()); } // Cross type comparison, Numeric token is always less than Text tokens. return this.getKind().compareTo(other.getKind()); } } ================================================ FILE: common/src/main/java/com/google/tsunami/common/version/Version.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.version; import static com.google.common.base.Preconditions.checkArgument; import com.google.auto.value.AutoValue; import com.google.auto.value.extension.memoized.Memoized; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.errorprone.annotations.Immutable; import java.util.Arrays; import java.util.regex.Pattern; /** * Software version class that support 3 types. Type {@link Type#NORMAL} with a version number, type * {@link Type#MAXIMUM} and {@link Type#MINIMUM} for use in version range to indicate unbounded * ranges. * *

Version is suitable for unstructured version string and supports comparison operations using * extra logic that detects semantic qualifier. * *

The current logic support comparison of versions that respect order, like: * *

 *   10
 *   1.1
 *   1.1a
 *   1.1a1
 *   20170101
 *   2017.01.01
 *   1.1rc1
 *   1.1-1p1
 *   1.1patch1
 *   1.1gg1.0
 *   1-1
 *   1:1.1
 *   1.1.alpha
 *   1.1.beta.1
 *   1.1.alpha.beta
 * 
* * Known limitation of this approach are versions with no order, like commit hashes. */ @AutoValue @Immutable public abstract class Version implements Comparable { private static final Pattern EPOCH_PATTERN = Pattern.compile("\\d+[:|_].*"); private static final Pattern SEMANTIC_SEGMENT_SEPARATORS = Pattern.compile("[-:_~]"); private static final Version MAXIMUM = builder().setVersionType(Type.MAXIMUM).setVersionString("").build(); private static final Version MINIMUM = builder().setVersionType(Type.MINIMUM).setVersionString("").build(); /** * Software version class that support 3 types. Type {@link Type#NORMAL} with a version number, * type {@link Type#MAXIMUM} and {@link Type#MINIMUM} for use in version range to indicate * unbounded ranges. */ public enum Type { NORMAL, MINIMUM, MAXIMUM } abstract Type versionType(); public abstract String versionString(); @Memoized ImmutableList segments() { String normalizedString = versionString(); // Add a default epoch of 0 if one is missing. if (!EPOCH_PATTERN.matcher(normalizedString).matches()) { normalizedString = "0:" + normalizedString; } return Arrays.stream(SEMANTIC_SEGMENT_SEPARATORS.split(normalizedString)) .filter(segment -> !segment.isEmpty()) .map(Segment::fromString) .filter(segment -> !segment.equals(Segment.NULL)) .collect(ImmutableList.toImmutableList()); } public static Builder builder() { return new AutoValue_Version.Builder(); } public static Version fromString(String versionString) { checkArgument(!Strings.isNullOrEmpty(versionString)); Version version = builder().setVersionType(Type.NORMAL).setVersionString(versionString).build(); if (!EPOCH_PATTERN.matcher(versionString).matches()) { versionString = "0:" + versionString; } boolean isValid = version.segments().stream() .flatMap(segment -> segment.tokens().stream()) .anyMatch( token -> (token.isNumeric() && token.getNumeric() != 0) || (token.isText() && !token.getText().isEmpty())); if (!isValid) { throw new IllegalArgumentException( String.format( "Input version string %s is not valid, it should contain at least one non-empty" + " field.", versionString)); } return version; } public static Version maximum() { return MAXIMUM; } public boolean isMaximum() { return versionType().equals(Type.MAXIMUM); } public static Version minimum() { return MINIMUM; } public boolean isMinimum() { return versionType().equals(Type.MINIMUM); } /** * Compare this Version object with the other one using their meaningful segments. * *

IMPORTANT: This compareTo implementation is NOT consistent with {@link * Version#equals(Object)} method, i.e. this.compareTo(that) == 0 does not imply * this.equals(that). The reason is that the raw version string is tokenized and certain tokens * are ignored. Tokenized strings are used for {@link #compareTo} comparison while raw version * string is used for {@link #equals} comparison. Be careful when using {@link Version} object in * collections like {@code HashMap} or {@code TreeMap}. * * @param other the other {@link Version} object to be compared with. * @return 0 if the segments of the two {@link Version} objects are the same, -1 if this {@link * Version} is less than {@code other}, 1 if this {@link Version} is greater than {@code * other}. */ @Override public int compareTo(Version other) { if ((this.isMinimum() && other.isMinimum()) || (this.isMaximum() && other.isMaximum())) { return 0; } if (this.isMinimum() || other.isMaximum()) { return -1; } if (this.isMaximum() || other.isMinimum()) { return 1; } return ComparisonUtility.compareListWithFillValue( this.segments(), other.segments(), Segment.NULL); } public boolean isLessThan(Version version) { return this.compareTo(version) < 0; } /** Builder for {@link Version}. */ @AutoValue.Builder public abstract static class Builder { public abstract Builder setVersionType(Type value); public abstract Builder setVersionString(String value); public abstract Version build(); } } ================================================ FILE: common/src/main/java/com/google/tsunami/common/version/VersionRange.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.version; import static com.google.common.base.Preconditions.checkArgument; import com.google.auto.value.AutoValue; import com.google.common.base.CharMatcher; import com.google.common.base.Strings; import com.google.errorprone.annotations.Immutable; /** Immutable version range, e.g. [1.0, 2.0), (,3.0), etc. */ @AutoValue @Immutable public abstract class VersionRange { /** The inclusiveness of the range endpoint. */ public enum Inclusiveness { INCLUSIVE, EXCLUSIVE } public abstract Version minVersion(); public abstract Inclusiveness minVersionInclusiveness(); public abstract Version maxVersion(); public abstract Inclusiveness maxVersionInclusiveness(); public static Builder builder() { return new AutoValue_VersionRange.Builder(); } /** Builder for {@link VersionRange}. */ @AutoValue.Builder public abstract static class Builder { public abstract Builder setMinVersion(Version value); public abstract Builder setMinVersionInclusiveness(Inclusiveness value); public abstract Builder setMaxVersion(Version value); public abstract Builder setMaxVersionInclusiveness(Inclusiveness value); public abstract VersionRange build(); } /** * Parses the given {@code rangeString} and generates a {@link VersionRange} object. * *

Valid strings for a version range are like: * *

    *
  • (,1.0]: from negative infinity to version 1.0 (inclusive). *
  • (,1.0): from negative infinity to version 1.0 (exclusive). *
  • [1.0,): from version 1.0 (inclusive) to positive infinity. *
  • (1.0,): from version 1.0 (exclusive) to positive infinity. *
  • [1.0,2.0): from version 1.0 (inclusive) to version 2.0 (exclusive). *
* * @param rangeString the string representation of a version range. * @return the parsed {@link VersionRange} object from the given string. */ public static VersionRange parse(String rangeString) { validateRangeString(rangeString); Inclusiveness minVersionInclusiveness = rangeString.startsWith("[") ? Inclusiveness.INCLUSIVE : Inclusiveness.EXCLUSIVE; Inclusiveness maxVersionInclusiveness = rangeString.endsWith("]") ? Inclusiveness.INCLUSIVE : Inclusiveness.EXCLUSIVE; int commaIndex = rangeString.indexOf(','); String minVersionString = rangeString.substring(1, commaIndex).trim(); Version minVersion; if (minVersionString.isEmpty()) { minVersionInclusiveness = Inclusiveness.EXCLUSIVE; minVersion = Version.minimum(); } else { minVersion = Version.fromString(minVersionString); } String maxVersionString = rangeString.substring(commaIndex + 1, rangeString.length() - 1).trim(); Version maxVersion; if (maxVersionString.isEmpty()) { maxVersionInclusiveness = Inclusiveness.EXCLUSIVE; maxVersion = Version.maximum(); } else { maxVersion = Version.fromString(maxVersionString); } if (!minVersion.isLessThan(maxVersion)) { throw new IllegalArgumentException( String.format( "Min version in range must be less than max version in range, got '%s'", rangeString)); } return builder() .setMinVersion(minVersion) .setMinVersionInclusiveness(minVersionInclusiveness) .setMaxVersion(maxVersion) .setMaxVersionInclusiveness(maxVersionInclusiveness) .build(); } public static boolean isValidVersionRange(String rangeString) { try { parse(rangeString); return true; } catch (Throwable t) { return false; } } private static void validateRangeString(String rangeString) { checkArgument(!Strings.isNullOrEmpty(rangeString), "Range string cannot be empty."); // Version range string must start with '[' or '('. if (!rangeString.startsWith("[") && !rangeString.startsWith("(")) { throw new IllegalArgumentException( String.format("Version range must start with '[' or '(', got '%s'", rangeString)); } // Version range string must end with ']' or ')'. if (!rangeString.endsWith("]") && !rangeString.endsWith(")")) { throw new IllegalArgumentException( String.format("Version range must end with ']' or ')', got '%s'", rangeString)); } // Remove the leading and ending parenthesis and brackets. String trimmedRange = rangeString.substring(1, rangeString.length() - 1).trim(); // No more parenthesis and brackets in the string. if (CharMatcher.anyOf("[()]").matchesAnyOf(trimmedRange)) { throw new IllegalArgumentException( String.format( "Parenthesis and/or brackets not allowed within version range, got '%s'", rangeString)); } // Only one comma that separates the minimum and maximum. if (CharMatcher.is(',').countIn(trimmedRange) != 1) { throw new IllegalArgumentException( String.format("Invalid range of versions, got '%s'", rangeString)); } // Version range of minimum to maximum is not supported. if (trimmedRange.equals(",")) { throw new IllegalArgumentException( String.format("Infinity range is not supported, got '%s'", rangeString)); } } } ================================================ FILE: common/src/main/java/com/google/tsunami/common/version/VersionSet.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.version; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import com.google.auto.value.AutoValue; import com.google.common.base.CharMatcher; import com.google.common.collect.ImmutableList; import com.google.errorprone.annotations.Immutable; /** Immutable set of discrete versions and version ranges. */ @AutoValue @Immutable public abstract class VersionSet { public abstract ImmutableList versions(); public abstract ImmutableList versionRanges(); public static Builder builder() { return new AutoValue_VersionSet.Builder(); } /** Builder for {@link VersionSet}. */ @AutoValue.Builder public abstract static class Builder { abstract ImmutableList.Builder versionsBuilder(); public Builder addVersion(Version version) { versionsBuilder().add(version); return this; } abstract ImmutableList.Builder versionRangesBuilder(); public Builder addVersionRange(VersionRange versionRange) { versionRangesBuilder().add(versionRange); return this; } public abstract VersionSet build(); } public static VersionSet parse(ImmutableList versionAndRangesList) { checkNotNull(versionAndRangesList); checkArgument(!versionAndRangesList.isEmpty(), "Versions and ranges list cannot be empty."); VersionSet.Builder versionSetBuilder = VersionSet.builder(); for (String versionOrRangeString : versionAndRangesList) { if (isDiscreteVersion(versionOrRangeString)) { versionSetBuilder.addVersion(Version.fromString(versionOrRangeString)); } else if (VersionRange.isValidVersionRange(versionOrRangeString)) { versionSetBuilder.addVersionRange(VersionRange.parse(versionOrRangeString)); } else { throw new IllegalArgumentException( String.format( "String '%s' is neither a discrete string nor a version range.", versionOrRangeString)); } } return versionSetBuilder.build(); } private static boolean isDiscreteVersion(String versionOrRangeString) { return CharMatcher.anyOf("[()], ").matchesNoneOf(versionOrRangeString); } } ================================================ FILE: common/src/test/java/com/google/tsunami/common/cli/CliOptionsModuleTest.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.cli; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; import com.beust.jcommander.Parameter; import com.beust.jcommander.ParameterException; import com.beust.jcommander.Parameters; import com.google.common.base.Strings; import com.google.inject.CreationException; import com.google.inject.Guice; import com.google.inject.Injector; import io.github.classgraph.ClassGraph; import io.github.classgraph.ScanResult; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** Tests for {@link CliOptionsModule}. */ @RunWith(JUnit4.class) public class CliOptionsModuleTest { @Test public void configure_whenValidArgs_parsesSuccessfully() { try (ScanResult scanResult = new ClassGraph() .enableAllInfo() .whitelistClasses( TestOption.class.getTypeName(), TestOptionWithRequiredParam.class.getTypeName()) .scan()) { Injector injector = Guice.createInjector( new CliOptionsModule( scanResult, "test", new String[] {"--test=testoption", "--test_required=abc"})); TestOption testOption = injector.getInstance(TestOption.class); TestOptionWithRequiredParam testOptionWithRequiredParam = injector.getInstance(TestOptionWithRequiredParam.class); assertThat(testOption.test).isEqualTo("testoption"); assertThat(testOptionWithRequiredParam.testRequired).isEqualTo("abc"); } } @Test public void configure_whenMissingRequiredArgs_throwsException() { try (ScanResult scanResult = new ClassGraph() .enableAllInfo() .whitelistClasses( TestOption.class.getTypeName(), TestOptionWithRequiredParam.class.getTypeName()) .scan()) { CreationException ex = assertThrows( CreationException.class, () -> Guice.createInjector( new CliOptionsModule(scanResult, "test", new String[] {"--test=test"}))); assertThat(ex).hasCauseThat().isInstanceOf(ParameterException.class); } } @Test public void configure_whenInvalidArgs_throwsException() { try (ScanResult scanResult = new ClassGraph().enableAllInfo().whitelistClasses(TestOption.class.getTypeName()).scan()) { CreationException ex = assertThrows( CreationException.class, () -> Guice.createInjector( new CliOptionsModule(scanResult, "test", new String[] {"--test=invalid"}))); assertThat(ex).hasCauseThat().isInstanceOf(ParameterException.class); } } @Test public void configure_whenUnknownArgs_throwsException() { try (ScanResult scanResult = new ClassGraph().enableAllInfo().whitelistClasses(TestOption.class.getTypeName()).scan()) { CreationException ex = assertThrows( CreationException.class, () -> Guice.createInjector( new CliOptionsModule( scanResult, "test", new String[] {"--test=test", "--unknown=unknown"}))); assertThat(ex).hasCauseThat().isInstanceOf(ParameterException.class); } } @Test public void configure_whenCliOptionNoCorrectCtor_throwsException() { try (ScanResult scanResult = new ClassGraph() .enableAllInfo() .whitelistClasses(TestOptionWithoutNoArgumentCtor.class.getTypeName()) .scan()) { assertThrows( AssertionError.class, () -> Guice.createInjector( new CliOptionsModule(scanResult, "test", new String[] {}))); } } @Parameters(separators = "=") private static final class TestOption implements CliOption { @Parameter(names = "--test", description = "A test option") String test; @Override public void validate() { if (Strings.isNullOrEmpty(test)) { throw new ParameterException("Empty test param"); } if (!test.startsWith("test")) { throw new ParameterException("test param value must start with 'test'"); } } } @Parameters(separators = "=") private static final class TestOptionWithRequiredParam implements CliOption { @Parameter(names = "--test_required", description = "A required option", required = true) String testRequired; @Override public void validate() {} } @Parameters(separators = "=") private static final class TestOptionWithoutNoArgumentCtor implements CliOption { String testOption; TestOptionWithoutNoArgumentCtor(String testOption) { this.testOption = testOption; } @Override public void validate() {} } } ================================================ FILE: common/src/test/java/com/google/tsunami/common/command/CommandExecutorFactoryTest.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.command; import static com.google.common.truth.Truth.assertThat; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import org.mockito.Mockito; /** Tests for {@link CommandExecutorFactory}. */ @RunWith(JUnit4.class) public final class CommandExecutorFactoryTest { @Test public void getInstance_whenNoPreviousInstanceIsProvided_createsNewProcessExecutor() { CommandExecutor executor = CommandExecutorFactory.create("fakeArgs"); assertThat(executor).isNotNull(); } @Test public void getInstance_whenPreviousInstanceIsProvided_returnsProvidedInstance() { CommandExecutor executor = Mockito.mock(CommandExecutor.class); CommandExecutorFactory.setInstance(executor); assertThat(CommandExecutorFactory.create("fakeArgs")).isSameInstanceAs(executor); } } ================================================ FILE: common/src/test/java/com/google/tsunami/common/command/CommandExecutorTest.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.command; import static com.google.common.truth.Truth.assertThat; import java.io.IOException; import java.util.concurrent.ExecutionException; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** Tests for {@link CommandExecutor}. */ @RunWith(JUnit4.class) public final class CommandExecutorTest { @Test public void execute_always_startsProcessAndReturnsProcessInstance() throws IOException, InterruptedException, ExecutionException { CommandExecutor executor = new CommandExecutor("/bin/sh", "-c", "echo 1"); Process process = executor.execute(); process.waitFor(); assertThat(process.exitValue()).isEqualTo(0); } @Test public void executeAsync_always_startsProcessAndReturnsProcessInstance() throws IOException, InterruptedException, ExecutionException { CommandExecutor executor = new CommandExecutor("/bin/sh", "-c", "echo 1"); Process process = executor.executeAsync(); process.waitFor(); assertThat(process.exitValue()).isEqualTo(0); } @Test public void executeWithNoStreamCollection_always_startsProcessAndReturnsProcessInstance() throws IOException, InterruptedException, ExecutionException { CommandExecutor executor = new CommandExecutor("/bin/sh", "-c", "echo 1"); Process process = executor.executeWithNoStreamCollection(); process.waitFor(); assertThat(process.exitValue()).isEqualTo(0); } @Test public void getOutput_always_returnsExpect() throws IOException, InterruptedException, ExecutionException { CommandExecutor executor = new CommandExecutor("/bin/sh", "-c", "echo 1"); Process process = executor.execute(); process.waitFor(); assertThat(executor.getOutput()).isEqualTo("1\n"); } @Test public void getOutput_withMultipleGetOutputCalls_returnsExpect() throws IOException, InterruptedException, ExecutionException { CommandExecutor executor = new CommandExecutor("/bin/sh", "-c", "echo 1"); Process process = executor.execute(); process.waitFor(); assertThat(executor.getOutput()).isEqualTo("1\n"); assertThat(executor.getOutput()).isEqualTo("1\n"); } @Test public void getError_always_returnsExpect() throws IOException, InterruptedException, ExecutionException { CommandExecutor executor = new CommandExecutor("/bin/sh", "-c", "echo 1 1>&2"); Process process = executor.execute(); process.waitFor(); assertThat(executor.getError()).isEqualTo("1\n"); } @Test public void getError_withMultipleGetOutputCalls_returnsExpect() throws IOException, InterruptedException, ExecutionException { CommandExecutor executor = new CommandExecutor("/bin/sh", "-c", "echo 1 1>&2"); Process process = executor.execute(); process.waitFor(); assertThat(executor.getError()).isEqualTo("1\n"); assertThat(executor.getError()).isEqualTo("1\n"); } } ================================================ FILE: common/src/test/java/com/google/tsunami/common/concurrent/BaseThreadPoolModuleTest.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.concurrent; import static org.junit.Assert.assertThrows; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.inject.AbstractModule; import com.google.inject.Key; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import javax.inject.Qualifier; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** Tests for {@link ThreadPoolModule}. */ @RunWith(JUnit4.class) public final class BaseThreadPoolModuleTest { /** Internal annotation used for tests. */ @Qualifier @Retention(RetentionPolicy.RUNTIME) @interface TestThreadPool {} static final class TestThreadPoolModule extends BaseThreadPoolModule { TestThreadPoolModule(Builder builder) { super(builder); } @Override void configureThreadPool(Key key) {} static final class Builder extends BaseThreadPoolModuleBuilder { Builder() { super(ListeningExecutorService.class); } @Override Builder self() { return this; } @Override void validate() {} @Override AbstractModule newModule() { return new TestThreadPoolModule(this); } } } @Test public void build_whenNoName_throwsIllegalStateException() { assertThrows(IllegalStateException.class, () -> new TestThreadPoolModule.Builder().build()); } @Test public void build_whenEmptyName_throwsIllegalArgumentException() { assertThrows( IllegalArgumentException.class, () -> new TestThreadPoolModule.Builder().setName("").build()); } @Test public void build_whenNegativeCoreSize_throwsIllegalArgumentException() { assertThrows( IllegalArgumentException.class, () -> new TestThreadPoolModule.Builder().setName("test").setCoreSize(-1).build()); } @Test public void build_whenNegativeMaxSize_throwsIllegalArgumentException() { assertThrows( IllegalArgumentException.class, () -> new TestThreadPoolModule.Builder().setName("test").setMaxSize(-1).build()); } @Test public void build_whenNegativeKeepAliveSeconds_throwsIllegalArgumentException() { assertThrows( IllegalArgumentException.class, () -> new TestThreadPoolModule.Builder() .setName("test") .setMaxSize(1) .setKeepAliveSeconds(-1) .build()); } @Test public void build_whenCoreSizeLessThanMaxSize_throwsIllegalStateException() { assertThrows( IllegalStateException.class, () -> new TestThreadPoolModule.Builder() .setName("test") .setCoreSize(2) .setMaxSize(1) .setAnnotation(TestThreadPool.class) .build()); } @Test public void build_whenNoAnnotation_throwsIllegalStateException() { assertThrows( IllegalStateException.class, () -> new TestThreadPoolModule.Builder().setName("test").build()); } } ================================================ FILE: common/src/test/java/com/google/tsunami/common/concurrent/ScheduledThreadPoolModuleTest.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.concurrent; import static com.google.common.truth.Truth.assertThat; import com.google.common.util.concurrent.ListeningScheduledExecutorService; import com.google.inject.Guice; import com.google.inject.Injector; import com.google.inject.Key; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.concurrent.ScheduledExecutorService; import javax.inject.Qualifier; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** Tests for {@link ScheduledThreadPoolModule}. */ @RunWith(JUnit4.class) public final class ScheduledThreadPoolModuleTest { /** Internal annotation used for tests. */ @Qualifier @Retention(RetentionPolicy.RUNTIME) @interface TestThreadPool {} @Test public void configure_always_bindsListeningScheduledExecutorService() { Injector injector = Guice.createInjector( new ScheduledThreadPoolModule.Builder() .setName("test") .setSize(1) .setAnnotation(TestThreadPool.class) .build()); assertThat( injector.getInstance(Key.get(ScheduledExecutorService.class, TestThreadPool.class))) .isInstanceOf(ListeningScheduledExecutorService.class); } @Test public void configure_always_bindsSingleton() { Injector injector = Guice.createInjector( new ScheduledThreadPoolModule.Builder() .setName("test") .setSize(1) .setAnnotation(TestThreadPool.class) .build()); ScheduledExecutorService scheduledExecutorService = injector.getInstance(Key.get(ScheduledExecutorService.class, TestThreadPool.class)); ListeningScheduledExecutorService listeningScheduledExecutorService = injector.getInstance( Key.get(ListeningScheduledExecutorService.class, TestThreadPool.class)); assertThat(scheduledExecutorService).isSameInstanceAs(listeningScheduledExecutorService); } } ================================================ FILE: common/src/test/java/com/google/tsunami/common/concurrent/ThreadPoolModuleTest.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.concurrent; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.inject.Guice; import com.google.inject.Injector; import com.google.inject.Key; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.PriorityBlockingQueue; import javax.inject.Qualifier; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** Tests for {@link ThreadPoolModule}. */ @RunWith(JUnit4.class) public final class ThreadPoolModuleTest { /** Internal annotation used for tests. */ @Qualifier @Retention(RetentionPolicy.RUNTIME) @interface TestThreadPool {} @Test public void configure_always_bindsListeningExecutorService() { Injector injector = Guice.createInjector( new ThreadPoolModule.Builder() .setName("test") .setCoreSize(1) .setMaxSize(1) .setAnnotation(TestThreadPool.class) .build()); assertThat(injector.getInstance(Key.get(Executor.class, TestThreadPool.class))) .isInstanceOf(ListeningExecutorService.class); assertThat(injector.getInstance(Key.get(ExecutorService.class, TestThreadPool.class))) .isInstanceOf(ListeningExecutorService.class); } @Test public void configure_always_bindsSingleton() { Injector injector = Guice.createInjector( new ThreadPoolModule.Builder() .setName("test") .setCoreSize(1) .setMaxSize(1) .setAnnotation(TestThreadPool.class) .build()); Executor executor = injector.getInstance(Key.get(Executor.class, TestThreadPool.class)); ExecutorService executorService = injector.getInstance(Key.get(ExecutorService.class, TestThreadPool.class)); ListeningExecutorService listeningExecutorService = injector.getInstance(Key.get(ListeningExecutorService.class, TestThreadPool.class)); assertThat(executor).isSameInstanceAs(listeningExecutorService); assertThat(executorService).isSameInstanceAs(listeningExecutorService); } @Test public void build_whenNegativeQueueCapacity_throwsIllegalArgumentException() { assertThrows( IllegalArgumentException.class, () -> new ThreadPoolModule.Builder() .setName("test") .setMaxSize(1) .setQueueCapacity(-1) .build()); } @Test public void build_whenBothQueueCapacityAndBlockingQueueSet_throwsIllegalStateException() { assertThrows( IllegalStateException.class, () -> new ThreadPoolModule.Builder() .setMaxSize(1) .setQueueCapacity(1) .setBlockingQueue(new PriorityBlockingQueue<>(1)) .build()); } @Test public void build_whenUnBoundedQueue_throwsIllegalStateException() { assertThrows( IllegalStateException.class, () -> new ThreadPoolModule.Builder() .setName("test") .setCoreSize(1) .setMaxSize(2) .setAnnotation(TestThreadPool.class) .build()); } } ================================================ FILE: common/src/test/java/com/google/tsunami/common/config/ConfigModuleTest.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.config; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; import com.google.common.collect.ImmutableMap; import com.google.inject.Guice; import com.google.inject.Injector; import com.google.tsunami.common.config.annotations.ConfigProperties; import io.github.classgraph.ClassGraph; import io.github.classgraph.ScanResult; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** Tests for {@link ConfigModule}. */ @RunWith(JUnit4.class) public final class ConfigModuleTest { @Test public void configure_always_bindsGivenTsunamiConfigObject() { TsunamiConfig tsunamiConfig = TsunamiConfig.fromYamlData(ImmutableMap.of()); try (ScanResult scanResult = new ClassGraph() .enableAllInfo() .whitelistClasses(TestConfigWithoutPrefix.class.getTypeName()) .scan()) { Injector injector = Guice.createInjector(new ConfigModule(scanResult, tsunamiConfig)); TsunamiConfig boundTsunamiConfig = injector.getInstance(TsunamiConfig.class); assertThat(boundTsunamiConfig).isSameInstanceAs(tsunamiConfig); } } @Test public void configure_whenValidConfigData_bindsSuccessfully() { TsunamiConfig tsunamiConfig = TsunamiConfig.fromYamlData(ImmutableMap.of("string_config", "testString")); try (ScanResult scanResult = new ClassGraph() .enableAllInfo() .whitelistClasses(TestConfigWithoutPrefix.class.getTypeName()) .scan()) { Injector injector = Guice.createInjector(new ConfigModule(scanResult, tsunamiConfig)); TestConfigWithoutPrefix testConfig = injector.getInstance(TestConfigWithoutPrefix.class); assertThat(testConfig.stringConfig).isEqualTo("testString"); } } @Test public void configure_whenMissingMatchedConfigData_bindsObjectWithDefaultValue() { TsunamiConfig tsunamiConfig = TsunamiConfig.fromYamlData(ImmutableMap.of()); try (ScanResult scanResult = new ClassGraph() .enableAllInfo() .whitelistClasses(TestConfigWithoutPrefix.class.getTypeName()) .scan()) { Injector injector = Guice.createInjector(new ConfigModule(scanResult, tsunamiConfig)); TestConfigWithoutPrefix testConfig = injector.getInstance(TestConfigWithoutPrefix.class); assertThat(testConfig.stringConfig).isNull(); } } @Test public void configure_whenValidConfigDataWithPrefix_bindsSuccessfully() { TsunamiConfig tsunamiConfig = TsunamiConfig.fromYamlData( ImmutableMap.of( "test", ImmutableMap.of("prefix", ImmutableMap.of("string_config", "testString")))); try (ScanResult scanResult = new ClassGraph() .enableAllInfo() .whitelistClasses(TestConfigWithPrefix.class.getTypeName()) .scan()) { Injector injector = Guice.createInjector(new ConfigModule(scanResult, tsunamiConfig)); TestConfigWithPrefix testConfig = injector.getInstance(TestConfigWithPrefix.class); assertThat(testConfig.stringConfig).isEqualTo("testString"); } } @Test public void configure_whenInvalidConfigClass_throwsException() { TsunamiConfig tsunamiConfig = TsunamiConfig.fromYamlData(ImmutableMap.of("string_config", "testString")); try (ScanResult scanResult = new ClassGraph() .enableAllInfo() .whitelistClasses(InvalidConfig.class.getTypeName()) .scan()) { assertThrows( AssertionError.class, () -> Guice.createInjector(new ConfigModule(scanResult, tsunamiConfig))); } } @ConfigProperties("") private static final class TestConfigWithoutPrefix { String stringConfig; } @ConfigProperties("test.prefix") private static final class TestConfigWithPrefix { String stringConfig; } @ConfigProperties("") private static final class InvalidConfig { String stringConfig; InvalidConfig(String stringConfig) { this.stringConfig = stringConfig; } } } ================================================ FILE: common/src/test/java/com/google/tsunami/common/config/TsunamiConfigTest.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.config; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import java.util.List; import java.util.Map; import org.junit.After; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** Tests for {@link TsunamiConfig}. */ @RunWith(JUnit4.class) public final class TsunamiConfigTest { private static final String TEST_PROPERTY = "test.property"; @After public void tearDown() { System.clearProperty(TEST_PROPERTY); } @Test public void fromYamlData_always_createTsunamiConfigFromMapData() { TsunamiConfig tsunamiConfig = TsunamiConfig.fromYamlData(ImmutableMap.of("test", "value")); assertThat(tsunamiConfig.getRawConfigData()).containsEntry("test", "value"); } @Test public void fromYamlData_whenNullYamlData_createEmptyConfigData() { TsunamiConfig tsunamiConfig = TsunamiConfig.fromYamlData(null); assertThat(tsunamiConfig.getRawConfigData()).isEmpty(); } @Test public void getSystemProperty_whenPropertyExists_returnsPropertyValue() { System.setProperty(TEST_PROPERTY, "Test value"); assertThat(TsunamiConfig.getSystemProperty(TEST_PROPERTY)).hasValue("Test value"); } @Test public void getSystemProperty_whenPropertyNotExists_returnsEmptyOptional() { assertThat(TsunamiConfig.getSystemProperty(TEST_PROPERTY)).isEmpty(); } @Test public void getSystemProperty_whenPropertyNotExistsWithDefaultValue_returnsDefaultValue() { assertThat(TsunamiConfig.getSystemProperty(TEST_PROPERTY, "Default")).isEqualTo("Default"); } @Test public void getConfig_whenValidConfigData_returnsBoundConfigObject() { TsunamiConfig tsunamiConfig = TsunamiConfig.fromYamlData(ImmutableMap.of("string", "string_value", "number", 1234)); SimpleConfig config = tsunamiConfig.getConfig("", SimpleConfig.class); assertThat(config.string).isEqualTo("string_value"); assertThat(config.number).isEqualTo(1234); } @Test public void getConfig_whenValidConfigDataAndPrefix_returnsBoundConfigObject() { TsunamiConfig tsunamiConfig = TsunamiConfig.fromYamlData( ImmutableMap.of( "test", ImmutableMap.of( "prefix", ImmutableMap.of("string", "string_value", "number", 1234)))); SimpleConfig config = tsunamiConfig.getConfig("test.prefix", SimpleConfig.class); assertThat(config.string).isEqualTo("string_value"); assertThat(config.number).isEqualTo(1234); } @Test public void getConfig_whenRequestedConfigNotExists_returnsObjectWithDefaultValue() { TsunamiConfig tsunamiConfig = TsunamiConfig.fromYamlData( ImmutableMap.of( "test", ImmutableMap.of( "prefix", ImmutableMap.of("string", "string_value", "number", 1234)))); SimpleConfig config = tsunamiConfig.getConfig("not.exist.prefix", SimpleConfig.class); assertThat(config.string).isNull(); assertThat(config.number).isEqualTo(0); } @Test public void getConfig_whenConfigHasComplicateDataStructure_returnsValidObject() { ImmutableList stringsField = ImmutableList.of("a", "b", "c"); ImmutableMap> complicateField = ImmutableMap.of("keyA", ImmutableList.of(1L, 2L), "keyB", ImmutableList.of(123L)); TsunamiConfig tsunamiConfig = TsunamiConfig.fromYamlData( ImmutableMap.of( "strings", stringsField, "complicateField", complicateField)); CollectionConfig config = tsunamiConfig.getConfig("", CollectionConfig.class); assertThat(config.strings).isEqualTo(stringsField); assertThat(config.complicateField).isEqualTo(complicateField); } @Test public void getConfig_whenConfigDataUseLowerUnderscoreCase_returnsValidObject() { ImmutableList stringsField = ImmutableList.of("a", "b", "c"); ImmutableMap> complicateField = ImmutableMap.of("keyA", ImmutableList.of(1L, 2L), "keyB", ImmutableList.of(123L)); TsunamiConfig tsunamiConfig = TsunamiConfig.fromYamlData( ImmutableMap.of( "strings", stringsField, "complicate_field", complicateField)); CollectionConfig config = tsunamiConfig.getConfig("", CollectionConfig.class); assertThat(config.strings).isEqualTo(stringsField); assertThat(config.complicateField).isEqualTo(complicateField); } @Test public void getConfig_whenRequestedConfigHasInvalidType_throwsException() { TsunamiConfig tsunamiConfig = TsunamiConfig.fromYamlData( ImmutableMap.of("test", ImmutableMap.of("prefix", "invalid_type"))); assertThrows( ConfigException.class, () -> tsunamiConfig.getConfig("test.prefix", SimpleConfig.class)); } @Test public void getConfig_whenUnassignableConfigValue_throwsException() { TsunamiConfig tsunamiConfig = TsunamiConfig.fromYamlData( ImmutableMap.of("string", "string_value", "number", "incompatible_value")); assertThrows( IllegalArgumentException.class, () -> tsunamiConfig.getConfig("", SimpleConfig.class)); } @Test public void getConfig_whenInvalidConfigObject_throwsException() { TsunamiConfig tsunamiConfig = TsunamiConfig.fromYamlData(ImmutableMap.of("string", "string_value")); assertThrows(AssertionError.class, () -> tsunamiConfig.getConfig("", InvalidConfig.class)); } private static final class SimpleConfig { String string; long number; } private static final class CollectionConfig { List strings; Map> complicateField; } private static final class InvalidConfig { String field; InvalidConfig(String field) { this.field = field; } } } ================================================ FILE: common/src/test/java/com/google/tsunami/common/config/YamlConfigLoaderTest.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.config; import static com.google.common.truth.Truth.assertThat; import static java.nio.charset.StandardCharsets.UTF_8; import com.google.common.io.Files; import java.io.File; import java.io.IOException; import org.junit.After; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** Tests for {@link YamlConfigLoader}. */ @RunWith(JUnit4.class) public final class YamlConfigLoaderTest { private static final String YAML_DATA = "test: \"data\"\ntest2: 123"; @After public void tearDown() { System.clearProperty("tsunami.config.location"); } @Test public void loadConfig_whenValidYamlFile_loadsConfigFromFile() throws IOException { File configFile = File.createTempFile("YamlConfigLoaderTest", ".yaml"); Files.asCharSink(configFile, UTF_8).write(YAML_DATA); System.setProperty("tsunami.config.location", configFile.getAbsolutePath()); TsunamiConfig tsunamiConfig = new YamlConfigLoader().loadConfig(); assertThat(tsunamiConfig.getRawConfigData()).containsExactly("test", "data", "test2", 123); } @Test public void loadConfig_whenYamlFileNotFound_usesEmptyConfig() { TsunamiConfig tsunamiConfig = new YamlConfigLoader().loadConfig(); assertThat(tsunamiConfig.getRawConfigData()).isEmpty(); } } ================================================ FILE: common/src/test/java/com/google/tsunami/common/data/NetworkEndpointUtilsTest.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.data; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat; import static org.junit.Assert.assertThrows; import com.google.common.net.HostAndPort; import com.google.tsunami.proto.AddressFamily; import com.google.tsunami.proto.Hostname; import com.google.tsunami.proto.IpAddress; import com.google.tsunami.proto.NetworkEndpoint; import com.google.tsunami.proto.Port; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** Tests for {@link NetworkEndpointUtils}. */ @RunWith(JUnit4.class) public class NetworkEndpointUtilsTest { @Test public void isIpV6Endpoint_withIpV4Endpoint_returnsFalse() { NetworkEndpoint ipV4Endpoint = NetworkEndpoint.newBuilder() .setType(NetworkEndpoint.Type.IP) .setIpAddress( IpAddress.newBuilder().setAddress("1.2.3.4").setAddressFamily(AddressFamily.IPV4)) .build(); assertThat(NetworkEndpointUtils.isIpV6Endpoint(ipV4Endpoint)).isFalse(); } @Test public void isIpV6Endpoint_withIpV4AndPortEndpoint_returnsFalse() { NetworkEndpoint ipV4AndPortEndpoint = NetworkEndpoint.newBuilder() .setType(NetworkEndpoint.Type.IP_PORT) .setPort(Port.newBuilder().setPortNumber(8888)) .setIpAddress( IpAddress.newBuilder().setAddress("1.2.3.4").setAddressFamily(AddressFamily.IPV4)) .build(); assertThat(NetworkEndpointUtils.isIpV6Endpoint(ipV4AndPortEndpoint)).isFalse(); } @Test public void isIpV6Endpoint_withIpV6Endpoint_returnsFalse() { NetworkEndpoint ipV6Endpoint = NetworkEndpoint.newBuilder() .setType(NetworkEndpoint.Type.IP) .setIpAddress( IpAddress.newBuilder().setAddress("3ffe::1").setAddressFamily(AddressFamily.IPV6)) .build(); assertThat(NetworkEndpointUtils.isIpV6Endpoint(ipV6Endpoint)).isTrue(); } @Test public void isIpV6Endpoint_withIpV6AndPortEndpoint_returnsFalse() { NetworkEndpoint ipV6AndPortEndpoint = NetworkEndpoint.newBuilder() .setType(NetworkEndpoint.Type.IP_PORT) .setPort(Port.newBuilder().setPortNumber(8888)) .setIpAddress( IpAddress.newBuilder().setAddress("3ffe::1").setAddressFamily(AddressFamily.IPV6)) .build(); assertThat(NetworkEndpointUtils.isIpV6Endpoint(ipV6AndPortEndpoint)).isTrue(); } @Test public void isIpV6Endpoint_withHostnameEndpoint_returnsFalse() { NetworkEndpoint hostnameEndpoint = NetworkEndpoint.newBuilder() .setType(NetworkEndpoint.Type.HOSTNAME) .setHostname(Hostname.newBuilder().setName("localhost")) .build(); assertThat(NetworkEndpointUtils.isIpV6Endpoint(hostnameEndpoint)).isFalse(); } @Test public void isIpV6Endpoint_withHostnameAndPortEndpoint_returnsFalse() { NetworkEndpoint hostnameAndPortEndpoint = NetworkEndpoint.newBuilder() .setType(NetworkEndpoint.Type.HOSTNAME_PORT) .setPort(Port.newBuilder().setPortNumber(8888)) .setHostname(Hostname.newBuilder().setName("localhost")) .build(); assertThat(NetworkEndpointUtils.isIpV6Endpoint(hostnameAndPortEndpoint)).isFalse(); } @Test public void toUriString_withIpV4Endpoint_returnsIpAddress() { NetworkEndpoint ipV4Endpoint = NetworkEndpoint.newBuilder() .setType(NetworkEndpoint.Type.IP) .setIpAddress( IpAddress.newBuilder().setAddress("1.2.3.4").setAddressFamily(AddressFamily.IPV4)) .build(); assertThat(NetworkEndpointUtils.toUriAuthority(ipV4Endpoint)).isEqualTo("1.2.3.4"); } @Test public void toUriString_withIpV6Endpoint_returnsIpAddressWithBracket() { NetworkEndpoint ipV6Endpoint = NetworkEndpoint.newBuilder() .setType(NetworkEndpoint.Type.IP) .setIpAddress( IpAddress.newBuilder().setAddress("3ffe::1").setAddressFamily(AddressFamily.IPV6)) .build(); assertThat(NetworkEndpointUtils.toUriAuthority(ipV6Endpoint)).isEqualTo("[3ffe::1]"); } @Test public void toUriString_withIpV4AndPortEndpoint_returnsIpAddressAndPort() { NetworkEndpoint ipV4AndPortEndpoint = NetworkEndpoint.newBuilder() .setType(NetworkEndpoint.Type.IP_PORT) .setPort(Port.newBuilder().setPortNumber(8888)) .setIpAddress( IpAddress.newBuilder().setAddress("1.2.3.4").setAddressFamily(AddressFamily.IPV4)) .build(); assertThat(NetworkEndpointUtils.toUriAuthority(ipV4AndPortEndpoint)).isEqualTo("1.2.3.4:8888"); } @Test public void toUriString_withIpV6AndPortEndpoint_returnsIpAddressWithBracketAndPort() { NetworkEndpoint ipV6Endpoint = NetworkEndpoint.newBuilder() .setType(NetworkEndpoint.Type.IP_PORT) .setPort(Port.newBuilder().setPortNumber(8888)) .setIpAddress( IpAddress.newBuilder().setAddress("3ffe::1").setAddressFamily(AddressFamily.IPV6)) .build(); assertThat(NetworkEndpointUtils.toUriAuthority(ipV6Endpoint)).isEqualTo("[3ffe::1]:8888"); } @Test public void toUriString_withHostnameEndpoint_returnsHostname() { NetworkEndpoint hostnameEndpoint = NetworkEndpoint.newBuilder() .setType(NetworkEndpoint.Type.HOSTNAME) .setHostname(Hostname.newBuilder().setName("localhost")) .build(); assertThat(NetworkEndpointUtils.toUriAuthority(hostnameEndpoint)).isEqualTo("localhost"); } @Test public void toUriString_withHostnameAndPortEndpoint_returnsHostnameAndPort() { NetworkEndpoint hostnameAndPortEndpoint = NetworkEndpoint.newBuilder() .setType(NetworkEndpoint.Type.HOSTNAME_PORT) .setPort(Port.newBuilder().setPortNumber(8888)) .setHostname(Hostname.newBuilder().setName("localhost")) .build(); assertThat(NetworkEndpointUtils.toUriAuthority(hostnameAndPortEndpoint)) .isEqualTo("localhost:8888"); } @Test public void toHostAndPort_withIpAddress_returnsHostWithIp() { NetworkEndpoint ipV4Endpoint = NetworkEndpoint.newBuilder() .setType(NetworkEndpoint.Type.IP) .setIpAddress( IpAddress.newBuilder().setAddress("1.2.3.4").setAddressFamily(AddressFamily.IPV4)) .build(); assertThat(NetworkEndpointUtils.toHostAndPort(ipV4Endpoint)) .isEqualTo(HostAndPort.fromHost("1.2.3.4")); } @Test public void toHostAndPort_withIpAddressAndPort_returnsHostWithIpAndPort() { NetworkEndpoint ipV4AndPortEndpoint = NetworkEndpoint.newBuilder() .setType(NetworkEndpoint.Type.IP_PORT) .setPort(Port.newBuilder().setPortNumber(8888)) .setIpAddress( IpAddress.newBuilder().setAddress("1.2.3.4").setAddressFamily(AddressFamily.IPV4)) .build(); assertThat(NetworkEndpointUtils.toHostAndPort(ipV4AndPortEndpoint)) .isEqualTo(HostAndPort.fromParts("1.2.3.4", 8888)); } @Test public void toHostAndPort_withHostname_returnsHostWithHostname() { NetworkEndpoint hostnameEndpoint = NetworkEndpoint.newBuilder() .setType(NetworkEndpoint.Type.HOSTNAME) .setHostname(Hostname.newBuilder().setName("localhost")) .build(); assertThat(NetworkEndpointUtils.toHostAndPort(hostnameEndpoint)) .isEqualTo(HostAndPort.fromHost("localhost")); } @Test public void toHostAndPort_withHostnameAndPort_returnsHostWithHostnameAndPort() { NetworkEndpoint hostnameAndPortEndpoint = NetworkEndpoint.newBuilder() .setType(NetworkEndpoint.Type.HOSTNAME_PORT) .setPort(Port.newBuilder().setPortNumber(8888)) .setHostname(Hostname.newBuilder().setName("localhost")) .build(); assertThat(NetworkEndpointUtils.toHostAndPort(hostnameAndPortEndpoint)) .isEqualTo(HostAndPort.fromParts("localhost", 8888)); } @Test public void forIp_withIpV4Address_returnsIpV4NetworkEndpoint() { assertThat(NetworkEndpointUtils.forIp("1.2.3.4")) .isEqualTo( NetworkEndpoint.newBuilder() .setType(NetworkEndpoint.Type.IP) .setIpAddress( IpAddress.newBuilder() .setAddressFamily(AddressFamily.IPV4) .setAddress("1.2.3.4")) .build()); } @Test public void forIp_withIpV6Address_returnsIpV6NetworkEndpoint() { assertThat(NetworkEndpointUtils.forIp("3ffe::1")) .isEqualTo( NetworkEndpoint.newBuilder() .setType(NetworkEndpoint.Type.IP) .setIpAddress( IpAddress.newBuilder() .setAddressFamily(AddressFamily.IPV6) .setAddress("3ffe::1")) .build()); } @Test public void forIpAndPort_withIpV4AddressAndPort_returnsIpV4AndPortNetworkEndpoint() { assertThat(NetworkEndpointUtils.forIpAndPort("1.2.3.4", 8888)) .isEqualTo( NetworkEndpoint.newBuilder() .setType(NetworkEndpoint.Type.IP_PORT) .setPort(Port.newBuilder().setPortNumber(8888)) .setIpAddress( IpAddress.newBuilder() .setAddressFamily(AddressFamily.IPV4) .setAddress("1.2.3.4")) .build()); } @Test public void forIpAndPort_withIpV6AddressAndPort_returnsIpV6AndPortNetworkEndpoint() { assertThat(NetworkEndpointUtils.forIpAndPort("3ffe::1", 8888)) .isEqualTo( NetworkEndpoint.newBuilder() .setType(NetworkEndpoint.Type.IP_PORT) .setPort(Port.newBuilder().setPortNumber(8888)) .setIpAddress( IpAddress.newBuilder() .setAddressFamily(AddressFamily.IPV6) .setAddress("3ffe::1")) .build()); } @Test public void forHostname_withHostname_returnsHostnameNetworkEndpoint() { assertThat(NetworkEndpointUtils.forHostname("localhost")) .isEqualTo( NetworkEndpoint.newBuilder() .setType(NetworkEndpoint.Type.HOSTNAME) .setHostname(Hostname.newBuilder().setName("localhost")) .build()); } @Test public void forHostnameAndPort_withHostnameAndPort_returnsHostnameAndPortNetworkEndpoint() { assertThat(NetworkEndpointUtils.forHostnameAndPort("localhost", 8888)) .isEqualTo( NetworkEndpoint.newBuilder() .setType(NetworkEndpoint.Type.HOSTNAME_PORT) .setPort(Port.newBuilder().setPortNumber(8888)) .setHostname(Hostname.newBuilder().setName("localhost")) .build()); } @Test public void forIpAndHostname_returnsIpAndHostnameNetworkEndpoint() { assertThat(NetworkEndpointUtils.forIpAndHostname("1.2.3.4", "host.com")) .isEqualTo( NetworkEndpoint.newBuilder() .setType(NetworkEndpoint.Type.IP_HOSTNAME) .setIpAddress( IpAddress.newBuilder() .setAddressFamily(AddressFamily.IPV4) .setAddress("1.2.3.4")) .setHostname(Hostname.newBuilder().setName("host.com")) .build()); } @Test public void forIpHostnameAndPort_returnsIpHostnameAndPortNetworkEndpoint() { assertThat(NetworkEndpointUtils.forIpHostnameAndPort("1.2.3.4", "host.com", 8888)) .isEqualTo( NetworkEndpoint.newBuilder() .setType(NetworkEndpoint.Type.IP_HOSTNAME_PORT) .setIpAddress( IpAddress.newBuilder() .setAddressFamily(AddressFamily.IPV4) .setAddress("1.2.3.4")) .setHostname(Hostname.newBuilder().setName("host.com")) .setPort(Port.newBuilder().setPortNumber(8888)) .build()); } @Test public void forNetworkEndpointAndPort_withIpEndpointAndPort_returnsIpAndPort() { NetworkEndpoint ipEndpoint = NetworkEndpointUtils.forIp("1.2.3.4"); assertThat(NetworkEndpointUtils.forNetworkEndpointAndPort(ipEndpoint, 8888)) .isEqualTo( NetworkEndpoint.newBuilder() .setType(NetworkEndpoint.Type.IP_PORT) .setPort(Port.newBuilder().setPortNumber(8888)) .setIpAddress( IpAddress.newBuilder() .setAddressFamily(AddressFamily.IPV4) .setAddress("1.2.3.4")) .build()); } @Test public void forNetworkEndpointAndPort_withHostnameEndpointAndPort_returnsHostnameAndPort() { NetworkEndpoint hostnameEndpoint = NetworkEndpointUtils.forHostname("localhost"); assertThat(NetworkEndpointUtils.forNetworkEndpointAndPort(hostnameEndpoint, 8888)) .isEqualTo( NetworkEndpoint.newBuilder() .setType(NetworkEndpoint.Type.HOSTNAME_PORT) .setPort(Port.newBuilder().setPortNumber(8888)) .setHostname(Hostname.newBuilder().setName("localhost")) .build()); } @Test public void forIp_withInvalidIp_throwsIllegalArgumentException() { assertThrows(IllegalArgumentException.class, () -> NetworkEndpointUtils.forIp("abc")); } @Test public void forIpAndPort_withInvalidIp_throwsIllegalArgumentException() { assertThrows( IllegalArgumentException.class, () -> NetworkEndpointUtils.forIpAndPort("abc", 8888)); } @Test public void forIpAndPort_withInvalidPort_throwsIllegalArgumentException() { assertThrows( IllegalArgumentException.class, () -> NetworkEndpointUtils.forIpAndPort("abc", -1)); assertThrows( IllegalArgumentException.class, () -> NetworkEndpointUtils.forIpAndPort("abc", 65536)); } @Test public void forHostname_withIpAddress_throwsIllegalArgumentException() { assertThrows(IllegalArgumentException.class, () -> NetworkEndpointUtils.forHostname("1.2.3.4")); assertThrows(IllegalArgumentException.class, () -> NetworkEndpointUtils.forHostname("3ffe::1")); } @Test public void forHostnameAndPort_withIpAddress_throwsIllegalArgumentException() { assertThrows( IllegalArgumentException.class, () -> NetworkEndpointUtils.forHostnameAndPort("1.2.3.4", 8888)); assertThrows( IllegalArgumentException.class, () -> NetworkEndpointUtils.forHostnameAndPort("3ffe::1", 8888)); } @Test public void forHostnameAndPort_withInvalidPort_throwsIllegalArgumentException() { assertThrows( IllegalArgumentException.class, () -> NetworkEndpointUtils.forHostnameAndPort("abc", -1)); assertThrows( IllegalArgumentException.class, () -> NetworkEndpointUtils.forHostnameAndPort("abc", 65536)); } @Test public void forNetworkEndpointAndPort_withInvalidEndpointType_throwsIllegalArgumentException() { assertThrows( IllegalArgumentException.class, () -> NetworkEndpointUtils.forNetworkEndpointAndPort( NetworkEndpointUtils.forIpAndPort("1.2.3.4", 80), 8888)); assertThrows( IllegalArgumentException.class, () -> NetworkEndpointUtils.forNetworkEndpointAndPort( NetworkEndpointUtils.forHostnameAndPort("localhost", 80), 8888)); } @Test public void forNetworkEndpointAndPort_withInvalidPort_throwsIllegalArgumentException() { assertThrows( IllegalArgumentException.class, () -> NetworkEndpointUtils.forNetworkEndpointAndPort( NetworkEndpointUtils.forIp("1.2.3.4"), -1)); assertThrows( IllegalArgumentException.class, () -> NetworkEndpointUtils.forNetworkEndpointAndPort( NetworkEndpointUtils.forHostname("localhost"), 65536)); } } ================================================ FILE: common/src/test/java/com/google/tsunami/common/data/NetworkServiceUtilsTest.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.data; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat; import static com.google.tsunami.common.data.NetworkEndpointUtils.forIpAndPort; import com.google.tsunami.proto.AddressFamily; import com.google.tsunami.proto.Hostname; import com.google.tsunami.proto.IpAddress; import com.google.tsunami.proto.NetworkEndpoint; import com.google.tsunami.proto.NetworkService; import com.google.tsunami.proto.Port; import com.google.tsunami.proto.ServiceContext; import com.google.tsunami.proto.Software; import com.google.tsunami.proto.TransportProtocol; import com.google.tsunami.proto.WebServiceContext; import java.io.IOException; import java.net.Inet4Address; import java.net.InetAddress; import java.net.URL; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** Tests for {@link NetworkServiceUtils}. */ @RunWith(JUnit4.class) public final class NetworkServiceUtilsTest { @Test public void isWebService_whenHttpService_returnsTrue() { assertThat( NetworkServiceUtils.isWebService( NetworkService.newBuilder().setServiceName("http").build())) .isTrue(); } @Test public void isWebService_whenHttpAltService_returnsTrue() { assertThat( NetworkServiceUtils.isWebService( NetworkService.newBuilder().setServiceName("http-alt").build())) .isTrue(); } @Test public void isWebService_whenHttpProxyService_returnsTrue() { assertThat( NetworkServiceUtils.isWebService( NetworkService.newBuilder().setServiceName("http-proxy").build())) .isTrue(); } @Test public void isWebService_whenHttpsService_returnsTrue() { assertThat( NetworkServiceUtils.isWebService( NetworkService.newBuilder().setServiceName("https").build())) .isTrue(); } @Test public void isWebService_whenRadanHttpService_returnsTrue() { assertThat( NetworkServiceUtils.isWebService( NetworkService.newBuilder().setServiceName("radan-http").build())) .isTrue(); } @Test public void isWebService_whenSslHttpService_returnsTrue() { assertThat( NetworkServiceUtils.isWebService( NetworkService.newBuilder().setServiceName("ssl/http").build())) .isTrue(); } @Test public void isWebService_whenSslHttpsService_returnsTrue() { assertThat( NetworkServiceUtils.isWebService( NetworkService.newBuilder().setServiceName("ssl/https").build())) .isTrue(); } @Test public void isWebService_whenCapitalizedHttpService_ignoresCaseAndReturnsTrue() { assertThat( NetworkServiceUtils.isWebService( NetworkService.newBuilder().setServiceName("HTTP").build())) .isTrue(); } @Test public void isWebService_whenHasAtLeastOneHttpMethod_returnsTrue() { assertThat( NetworkServiceUtils.isWebService( NetworkService.newBuilder() .setServiceName("irrelevantService") .addSupportedHttpMethods("IrrelevantMethodName") .build())) .isTrue(); } @Test public void isWebService_whenNonWebService_returnsFalse() { assertThat( NetworkServiceUtils.isWebService( NetworkService.newBuilder().setServiceName("ssh").build())) .isFalse(); } @Test public void isPlainHttp_whenPlainHttpService_returnsTrue() { assertThat( NetworkServiceUtils.isPlainHttp( NetworkService.newBuilder().setServiceName("http").build())) .isTrue(); } @Test public void isPlainHttp_whenHttpAltService_returnsTrue() { assertThat( NetworkServiceUtils.isPlainHttp( NetworkService.newBuilder().setServiceName("http-alt").build())) .isTrue(); } @Test public void isPlainHttp_whenHttpsService_returnsFalse() { assertThat( NetworkServiceUtils.isPlainHttp( NetworkService.newBuilder().setServiceName("https").build())) .isFalse(); } @Test public void isPlainHttp_whenRadanHttpService_returnsTrue() { assertThat( NetworkServiceUtils.isPlainHttp( NetworkService.newBuilder().setServiceName("radan-http").build())) .isTrue(); } @Test public void isPlainHttp_whenNonWebService_returnsFalse() { assertThat( NetworkServiceUtils.isPlainHttp( NetworkService.newBuilder().setServiceName("ssh").build())) .isFalse(); } @Test public void isPlainHttp_whenHttpServiceButHasSslVersions_returnsFalse() { assertThat( NetworkServiceUtils.isPlainHttp( NetworkService.newBuilder() .setServiceName("http") .addSupportedSslVersions("SSLV3") .build())) .isFalse(); } @Test public void isPlainHttp_whenNonHttpServiceButHasSslVersions_returnsFalse() { assertThat( NetworkServiceUtils.isPlainHttp( NetworkService.newBuilder() .setServiceName("ssh") .addSupportedSslVersions("SSLV3") .build())) .isFalse(); } @Test public void isPlainHttp_whenHttpServiceFromHttpMethodsWithoutSslVersions_returnsTrue() { assertThat( NetworkServiceUtils.isPlainHttp( NetworkService.newBuilder() .setServiceName("ssh") .addSupportedHttpMethods("GET") .build())) .isTrue(); } @Test public void isPlainHttp_whenHttpServiceWithSslVersions_returnsFalse() { assertThat( NetworkServiceUtils.isPlainHttp( NetworkService.newBuilder() .setServiceName("http") .addSupportedSslVersions("SSLV3") .build())) .isFalse(); } @Test public void getServiceName_whenNonWebService_returnsServiceName() { assertThat( NetworkServiceUtils.getServiceName( NetworkService.newBuilder() .setNetworkEndpoint(forIpAndPort("127.0.0.1", 22)) .setServiceName("ssh") .build())) .isEqualTo("ssh"); } @Test public void getServiceName_whenWebServiceNoSoftware_returnsServiceName() { assertThat( NetworkServiceUtils.getServiceName( NetworkService.newBuilder() .setNetworkEndpoint(forIpAndPort("127.0.0.1", 22)) .setServiceName("http") .build())) .isEqualTo("http"); } @Test public void getServiceName_whenWebServiceWithSoftware_returnsServiceName() { assertThat( NetworkServiceUtils.getServiceName( NetworkService.newBuilder() .setNetworkEndpoint(forIpAndPort("127.0.0.1", 22)) .setServiceName("http") .setSoftware(Software.newBuilder().setName("WordPress")) .build())) .isEqualTo("wordpress"); } @Test public void getWebServiceName_whenWebServiceWithSoftware_returnsWebServiceName() { assertThat( NetworkServiceUtils.getWebServiceName( NetworkService.newBuilder() .setNetworkEndpoint(forIpAndPort("127.0.0.1", 8080)) .setServiceName("http") .setServiceContext( ServiceContext.newBuilder() .setWebServiceContext( WebServiceContext.newBuilder() .setSoftware(Software.newBuilder().setName("jenkins")))) .build())) .isEqualTo("jenkins"); } @Test public void getServiceName_whenWebServiceNoContext_returnsServiceName() { assertThat( NetworkServiceUtils.getWebServiceName( NetworkService.newBuilder() .setNetworkEndpoint(forIpAndPort("127.0.0.1", 8080)) .setServiceName("http") .setSoftware(Software.newBuilder().setName("nothttp")) .build())) .isEqualTo("http"); } @Test public void buildWebApplicationRootUrl_whenHttpWithoutRoot_buildsExpectedUrl() { assertThat( NetworkServiceUtils.buildWebApplicationRootUrl( NetworkService.newBuilder() .setNetworkEndpoint(forIpAndPort("127.0.0.1", 8080)) .setServiceName("http") .build())) .isEqualTo("http://127.0.0.1:8080/"); } @Test public void buildWebApplicationRootUrl_whenHttpsWithoutRoot_buildsExpectedUrl() { assertThat( NetworkServiceUtils.buildWebApplicationRootUrl( NetworkService.newBuilder() .setNetworkEndpoint(forIpAndPort("127.0.0.1", 8443)) .setServiceName("ssl/https") .setServiceContext( ServiceContext.newBuilder() .setWebServiceContext( WebServiceContext.newBuilder().setApplicationRoot("test_root"))) .build())) .isEqualTo("https://127.0.0.1:8443/test_root/"); } @Test public void buildWebApplicationRootUrl_whenHttpWithRootPath_buildsUrlWithExpectedRoot() { assertThat( NetworkServiceUtils.buildWebApplicationRootUrl( NetworkService.newBuilder() .setNetworkEndpoint(forIpAndPort("127.0.0.1", 8080)) .setServiceName("http") .setServiceContext( ServiceContext.newBuilder() .setWebServiceContext( WebServiceContext.newBuilder().setApplicationRoot("/test_root"))) .build())) .isEqualTo("http://127.0.0.1:8080/test_root/"); } @Test public void buildWebApplicationRootUrl_whenRootPathNoLeadingSlash_appendsLeadingSlash() { assertThat( NetworkServiceUtils.buildWebApplicationRootUrl( NetworkService.newBuilder() .setNetworkEndpoint(forIpAndPort("127.0.0.1", 8080)) .setServiceName("http") .setServiceContext( ServiceContext.newBuilder() .setWebServiceContext( WebServiceContext.newBuilder().setApplicationRoot("test_root"))) .build())) .isEqualTo("http://127.0.0.1:8080/test_root/"); } @Test public void buildWebApplicationRootUrl_whenHttpServiceOnPort80_removesTrailingPortFromUrl() { assertThat( NetworkServiceUtils.buildWebApplicationRootUrl( NetworkService.newBuilder() .setNetworkEndpoint(forIpAndPort("127.0.0.1", 80)) .setServiceName("http") .setServiceContext( ServiceContext.newBuilder() .setWebServiceContext( WebServiceContext.newBuilder().setApplicationRoot("test_root"))) .build())) .isEqualTo("http://127.0.0.1/test_root/"); } @Test public void buildWebApplicationRootUrl_whenHttpsServiceOnPort443_removesTrailingPortFromUrl() { assertThat( NetworkServiceUtils.buildWebApplicationRootUrl( NetworkService.newBuilder() .setNetworkEndpoint(forIpAndPort("127.0.0.1", 443)) .setServiceName("ssl/https") .setServiceContext( ServiceContext.newBuilder() .setWebServiceContext( WebServiceContext.newBuilder().setApplicationRoot("test_root"))) .build())) .isEqualTo("https://127.0.0.1/test_root/"); } @Test public void buildWebApplicationRootUrl_whenNotWebService_returnsHttpUrl() { assertThat( NetworkServiceUtils.buildWebApplicationRootUrl( NetworkService.newBuilder() .setNetworkEndpoint(forIpAndPort("127.0.0.1", 2121)) .setServiceName("unknown") .build())) .isEqualTo("http://127.0.0.1:2121/"); } @Test public void buildUriNetworkService_returnsNetworkService() throws IOException { URL url = new URL("https://localhost/function1"); String hostname = url.getHost(); String ipaddress = InetAddress.getByName(hostname).getHostAddress(); InetAddress inetAddress = InetAddress.getByName(url.getHost()); AddressFamily addressFamily = inetAddress instanceof Inet4Address ? AddressFamily.IPV4 : AddressFamily.IPV6; NetworkEndpoint networkEndpoint = NetworkEndpoint.newBuilder() .setType(NetworkEndpoint.Type.IP_HOSTNAME_PORT) .setIpAddress( IpAddress.newBuilder().setAddressFamily(addressFamily).setAddress(ipaddress)) .setPort(Port.newBuilder().setPortNumber(443)) .setHostname(Hostname.newBuilder().setName("localhost")) .build(); NetworkService networkService = NetworkService.newBuilder() .setNetworkEndpoint(networkEndpoint) .setTransportProtocol(TransportProtocol.TCP) .setServiceName("https") .setServiceContext( ServiceContext.newBuilder() .setWebServiceContext( WebServiceContext.newBuilder().setApplicationRoot("/function1"))) .build(); assertThat(NetworkServiceUtils.buildUriNetworkService("https://localhost/function1")) .isEqualTo(networkService); } } ================================================ FILE: common/src/test/java/com/google/tsunami/common/io/archiving/ArchiverTestUtils.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.io.archiving; /** Utilities for all {@link Archiver} unit tests. */ final class ArchiverTestUtils { private ArchiverTestUtils() {} /** Returns a byte array of length size that has values 0 .. size - 1. */ static byte[] newPreFilledByteArray(int size) { return newPreFilledByteArray(0, size); } /** Returns a byte array of length size that has values offset .. offset + size - 1. */ static byte[] newPreFilledByteArray(int offset, int size) { byte[] array = new byte[size]; for (int i = 0; i < size; i++) { array[i] = (byte) (offset + i); } return array; } } ================================================ FILE: common/src/test/java/com/google/tsunami/common/io/archiving/GoogleCloudStorageArchiverTest.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.io.archiving; import static com.google.common.truth.Truth.assertThat; import static com.google.tsunami.common.io.archiving.ArchiverTestUtils.newPreFilledByteArray; import static java.nio.charset.StandardCharsets.UTF_8; import static org.junit.Assert.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import com.google.cloud.WriteChannel; import com.google.cloud.storage.BlobInfo; import com.google.cloud.storage.Storage; import com.google.inject.AbstractModule; import com.google.inject.Guice; import java.io.IOException; import java.nio.ByteBuffer; import javax.inject.Inject; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; /** Tests for {@link GoogleCloudStorageArchiver}. */ @RunWith(JUnit4.class) public final class GoogleCloudStorageArchiverTest { private static final String BUCKET_ID = "test_bucket"; private static final String OBJECT_ID = "test/object"; @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); @Mock Storage mockStorage; @Mock WriteChannel mockWriter; @Captor ArgumentCaptor blobInfoCaptor; @Captor ArgumentCaptor byteDataCaptor; @Captor ArgumentCaptor byteBufferCaptor; @Inject private GoogleCloudStorageArchiver.Options options; @Inject private GoogleCloudStorageArchiver.Factory archiverFactory; @Before public void setUp() { Guice.createInjector( new AbstractModule() { @Override protected void configure() { bind(GoogleCloudStorageArchiver.Options.class) .toInstance(new GoogleCloudStorageArchiver.Options()); install(new GoogleCloudStorageArchiverModule()); } }) .injectMembers(this); } @Test public void archive_withSmallSizeString_createsBlobInOneRequest() { GoogleCloudStorageArchiver archiver = archiverFactory.create(mockStorage); String dataToArchive = "TEST DATA"; boolean succeeded = archiver.archive(buildGcsUrl(BUCKET_ID, OBJECT_ID), dataToArchive); assertThat(succeeded).isTrue(); verify(mockStorage, times(1)).create(blobInfoCaptor.capture(), byteDataCaptor.capture()); assertThat(blobInfoCaptor.getValue()) .isEqualTo(BlobInfo.newBuilder(BUCKET_ID, OBJECT_ID).build()); assertThat(byteDataCaptor.getValue()).isEqualTo(dataToArchive.getBytes(UTF_8)); } @Test public void archive_withSmallSizeBlob_createsBlobInOneRequest() { GoogleCloudStorageArchiver archiver = archiverFactory.create(mockStorage); byte[] dataToArchive = newPreFilledByteArray(10); boolean succeeded = archiver.archive(buildGcsUrl(BUCKET_ID, OBJECT_ID), dataToArchive); assertThat(succeeded).isTrue(); verify(mockStorage, times(1)).create(blobInfoCaptor.capture(), byteDataCaptor.capture()); assertThat(blobInfoCaptor.getValue()) .isEqualTo(BlobInfo.newBuilder(BUCKET_ID, OBJECT_ID).build()); assertThat(byteDataCaptor.getValue()).isEqualTo(dataToArchive); } @Test public void archive_withLargeSizeString_createsBlobWithWriter() throws IOException { options.chunkSizeInBytes = 8; options.chunkUploadThresholdInBytes = 16; doReturn(mockWriter) .when(mockStorage) .writer(eq(BlobInfo.newBuilder(BUCKET_ID, OBJECT_ID).build())); GoogleCloudStorageArchiver archiver = archiverFactory.create(mockStorage); String dataToArchive = "THIS IS A LONG DATA"; int numOfChunks = (int) Math.ceil((double) dataToArchive.length() / options.chunkSizeInBytes); boolean succeeded = archiver.archive(buildGcsUrl(BUCKET_ID, OBJECT_ID), dataToArchive); assertThat(succeeded).isTrue(); verify(mockWriter, times(numOfChunks)).write(byteBufferCaptor.capture()); assertThat(byteBufferCaptor.getAllValues()) .containsExactly( ByteBuffer.wrap(dataToArchive.getBytes(UTF_8), 0, 8), ByteBuffer.wrap(dataToArchive.getBytes(UTF_8), 8, 8), ByteBuffer.wrap(dataToArchive.getBytes(UTF_8), 16, 3)); } @Test public void archive_withLargeSizeBlob_createsBlobWithWriter() throws IOException { options.chunkSizeInBytes = 8; options.chunkUploadThresholdInBytes = 16; doReturn(mockWriter) .when(mockStorage) .writer(eq(BlobInfo.newBuilder(BUCKET_ID, OBJECT_ID).build())); GoogleCloudStorageArchiver archiver = archiverFactory.create(mockStorage); byte[] dataToArchive = newPreFilledByteArray(20); int numOfChunks = (int) Math.ceil((double) dataToArchive.length / options.chunkSizeInBytes); boolean succeeded = archiver.archive(buildGcsUrl(BUCKET_ID, OBJECT_ID), dataToArchive); assertThat(succeeded).isTrue(); verify(mockWriter, times(numOfChunks)).write(byteBufferCaptor.capture()); assertThat(byteBufferCaptor.getAllValues()) .containsExactly( ByteBuffer.wrap(dataToArchive, 0, 8), ByteBuffer.wrap(dataToArchive, 8, 8), ByteBuffer.wrap(dataToArchive, 16, 4)); } @Test public void archive_withLargeSizeBlobAndWriteError_returnsFalse() throws IOException { options.chunkSizeInBytes = 8; options.chunkUploadThresholdInBytes = 16; doReturn(mockWriter) .when(mockStorage) .writer(eq(BlobInfo.newBuilder(BUCKET_ID, OBJECT_ID).build())); doThrow(IOException.class).when(mockWriter).write(any()); GoogleCloudStorageArchiver archiver = archiverFactory.create(mockStorage); byte[] dataToArchive = newPreFilledByteArray(20); assertThat(archiver.archive(buildGcsUrl(BUCKET_ID, OBJECT_ID), dataToArchive)).isFalse(); } @Test public void archive_withInvalidGcsUrl_throwsIllegalArgumentException() { GoogleCloudStorageArchiver archiver = archiverFactory.create(mockStorage); assertThrows(IllegalArgumentException.class, () -> archiver.archive("invalid_url", "")); } private static final String buildGcsUrl(String bucketId, String objectId) { return String.format("gs://%s/%s", bucketId, objectId); } } ================================================ FILE: common/src/test/java/com/google/tsunami/common/io/archiving/RawFileArchiverTest.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.io.archiving; import static com.google.common.truth.Truth.assertThat; import static com.google.tsunami.common.io.archiving.ArchiverTestUtils.newPreFilledByteArray; import static java.nio.charset.StandardCharsets.UTF_8; import com.google.common.io.Files; import java.io.File; import java.io.IOException; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** Tests for {@link RawFileArchiver}. */ @RunWith(JUnit4.class) public final class RawFileArchiverTest { @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); @Test public void archive_whenValidTargetFileAndByteArrayData_archivesGivenDataWithGivenName() throws IOException { File tempFile = temporaryFolder.newFile(); byte[] data = newPreFilledByteArray(200); RawFileArchiver rawFileArchiver = new RawFileArchiver(); assertThat(rawFileArchiver.archive(tempFile.getAbsolutePath(), data)).isTrue(); assertThat(Files.toByteArray(tempFile)).isEqualTo(data); } @Test public void archive_whenInvalidTargetFileAndByteArrayData_returnsFalse() throws IOException { File tempFile = temporaryFolder.newFile(); byte[] data = newPreFilledByteArray(200); RawFileArchiver rawFileArchiver = new RawFileArchiver(); assertThat(rawFileArchiver.archive(tempFile.getParent(), data)).isFalse(); assertThat(tempFile.length()).isEqualTo(0); } @Test public void archive_whenValidTargetFileAndCharSequenceData_archivesGivenDataWithGivenName() throws IOException { File tempFile = temporaryFolder.newFile(); String data = "file data"; RawFileArchiver rawFileArchiver = new RawFileArchiver(); assertThat(rawFileArchiver.archive(tempFile.getAbsolutePath(), data)).isTrue(); assertThat(Files.asCharSource(tempFile, UTF_8).read()).isEqualTo(data); } @Test public void archive_whenInvalidTargetFileAndCharSequenceData_returnsFalse() throws IOException { File tempFile = temporaryFolder.newFile(); String data = "file data"; RawFileArchiver rawFileArchiver = new RawFileArchiver(); assertThat(rawFileArchiver.archive(tempFile.getParent(), data)).isFalse(); assertThat(tempFile.length()).isEqualTo(0); } } ================================================ FILE: common/src/test/java/com/google/tsunami/common/net/FuzzingUtilsTest.java ================================================ /* * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.net; import static com.google.common.truth.Truth.assertThat; import com.google.common.collect.ImmutableList; import com.google.tsunami.common.net.http.HttpRequest; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @RunWith(JUnit4.class) public final class FuzzingUtilsTest { private static final HttpRequest REQUEST_WITHOUT_GET_PARAMETERS = HttpRequest.get("https://google.com").withEmptyHeaders().build(); private static final HttpRequest REQUEST_WITH_GET_PARAMETERS = HttpRequest.get("https://google.com?key=value&other=test").withEmptyHeaders().build(); @Test public void fuzzGetParametersWithDefaultParameter_whenNoGetParameters_addsDefaultParameter() { HttpRequest requestWithDefaultParameter = HttpRequest.get("https://google.com?default=").withEmptyHeaders().build(); assertThat( FuzzingUtils.fuzzGetParametersWithDefaultParameter( REQUEST_WITHOUT_GET_PARAMETERS, "", "default")) .contains(requestWithDefaultParameter); } @Test public void fuzzGetParametersWithDefaultParameter_whenGetParameters_doesNotAddDefaultParameter() { HttpRequest requestWithDefaultParameter = HttpRequest.get("https://google.com?default=").withEmptyHeaders().build(); assertThat( FuzzingUtils.fuzzGetParametersWithDefaultParameter( REQUEST_WITH_GET_PARAMETERS, "", "default")) .doesNotContain(requestWithDefaultParameter); } @Test public void fuzzGetParametersWithDefaultParameter_whenGetParameters_fuzzesAllParameters() { ImmutableList requestsWithFuzzedGetParameters = ImmutableList.of( HttpRequest.get("https://google.com?key=&other=test") .withEmptyHeaders() .build(), HttpRequest.get("https://google.com?key=value&other=") .withEmptyHeaders() .build()); assertThat( FuzzingUtils.fuzzGetParametersWithDefaultParameter( REQUEST_WITH_GET_PARAMETERS, "", "default")) .containsAtLeastElementsIn(requestsWithFuzzedGetParameters); } @Test public void fuzzGetParametersExpectingPathValues_whenGetParameterValueHasFileExtension_appendsFileExtensionToPayload() { HttpRequest requestWithFileExtension = HttpRequest.get("https://google.com?key=value.jpg").withEmptyHeaders().build(); HttpRequest requestWithFuzzedGetParameterWithFileExtension = HttpRequest.get("https://google.com?key=%00.jpg").withEmptyHeaders().build(); assertThat( FuzzingUtils.fuzzGetParametersExpectingPathValues( requestWithFileExtension, "")) .contains(requestWithFuzzedGetParameterWithFileExtension); } @Test public void fuzzGetParametersExpectingPathValues_whenGetParameterValueHasPathPrefix_prefixesPayload() { HttpRequest requestWithPathPrefix = HttpRequest.get("https://google.com?key=resources/value").withEmptyHeaders().build(); HttpRequest requestWithFuzzedGetParameterWithPathPrefix = HttpRequest.get("https://google.com?key=resources/").withEmptyHeaders().build(); assertThat( FuzzingUtils.fuzzGetParametersExpectingPathValues(requestWithPathPrefix, "")) .contains(requestWithFuzzedGetParameterWithPathPrefix); } @Test public void fuzzGetParametersExpectingPathValues_whenGetParameterValueHasPathPrefixAndFileExtension_prefixesPayloadAndAppendsFileExtension() { HttpRequest requestWithPathPrefixAndFileExtension = HttpRequest.get("https://google.com?key=resources/value.jpg").withEmptyHeaders().build(); HttpRequest requestWithFuzzedGetParameterWithPathPrefixAndFileExtension = HttpRequest.get("https://google.com?key=resources/%00.jpg") .withEmptyHeaders() .build(); assertThat( FuzzingUtils.fuzzGetParametersExpectingPathValues( requestWithPathPrefixAndFileExtension, "")) .contains(requestWithFuzzedGetParameterWithPathPrefixAndFileExtension); } @Test public void fuzzGetParametersExpectingPathValues_whenGetParameterValueHasPathPrefixOrFileExtension_prefixesPayloadOrAppendsFileExtension() { HttpRequest requestWithPathPrefixOrFileExtension = HttpRequest.get("https://google.com?key=resources./value").withEmptyHeaders().build(); HttpRequest requestWithFuzzedGetParameterWithPathPrefixAndFileExtension = HttpRequest.get("https://google.com?key=resources./%00./value") .withEmptyHeaders() .build(); assertThat( FuzzingUtils.fuzzGetParametersExpectingPathValues( requestWithPathPrefixOrFileExtension, "")) .doesNotContain(requestWithFuzzedGetParameterWithPathPrefixAndFileExtension); } @Test public void fuzzGetParameters_whenNoGetParameters_returnsEmptyList() { assertThat(FuzzingUtils.fuzzGetParameters(REQUEST_WITHOUT_GET_PARAMETERS, "")) .isEmpty(); } @Test public void fuzzGetParameters_whenGetParameters_fuzzesAllParameters() { ImmutableList requestsWithFuzzedGetParameters = ImmutableList.of( HttpRequest.get("https://google.com?key=&other=test") .withEmptyHeaders() .build(), HttpRequest.get("https://google.com?key=value&other=") .withEmptyHeaders() .build()); assertThat(FuzzingUtils.fuzzGetParameters(REQUEST_WITH_GET_PARAMETERS, "")) .containsAtLeastElementsIn(requestsWithFuzzedGetParameters); } } ================================================ FILE: common/src/test/java/com/google/tsunami/common/net/UrlUtilsTest.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.net; import static com.google.common.truth.Truth.assertThat; import static com.google.tsunami.common.net.UrlUtils.allSubPaths; import static com.google.tsunami.common.net.UrlUtils.removeLeadingSlashes; import static com.google.tsunami.common.net.UrlUtils.removeTrailingSlashes; import static com.google.tsunami.common.net.UrlUtils.urlEncode; import okhttp3.HttpUrl; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** Tests for {@link UrlUtils}. */ @RunWith(JUnit4.class) public final class UrlUtilsTest { @Test public void allSubPaths_whenInvalidUrl_returnsEmptyList() { assertThat(allSubPaths("invalid_url")).isEmpty(); } @Test public void allSubPaths_whenNoSubPathNoTrailingSlash_returnsSingleUrl() { assertThat(allSubPaths("http://localhost")).containsExactly(HttpUrl.parse("http://localhost/")); } @Test public void allSubPaths_whenNoSubPathWithTrailingSlash_returnsSingleUrl() { assertThat(allSubPaths("http://localhost/")) .containsExactly(HttpUrl.parse("http://localhost/")); } @Test public void allSubPaths_whenValidQueryParamsAndFragments_removesParamsAndFragments() { assertThat(allSubPaths("http://localhost/?param=value¶m2=value2#abc")) .containsExactly(HttpUrl.parse("http://localhost/")); } @Test public void allSubPaths_whenSingleSubPathsNoTrailingSlash_returnsExpectedUrl() { assertThat(allSubPaths("http://localhost/a")) .containsExactly(HttpUrl.parse("http://localhost/"), HttpUrl.parse("http://localhost/a/")); } @Test public void allSubPaths_whenSingleSubPathsWithTrailingSlash_returnsExpectedUrl() { assertThat(allSubPaths("http://localhost/a/")) .containsExactly(HttpUrl.parse("http://localhost/"), HttpUrl.parse("http://localhost/a/")); } @Test public void allSubPaths_whenMultipleSubPathsNoTrailingSlash_returnsExpectedUrl() { assertThat(allSubPaths("http://localhost/a/b/c")) .containsExactly( HttpUrl.parse("http://localhost/"), HttpUrl.parse("http://localhost/a/"), HttpUrl.parse("http://localhost/a/b/"), HttpUrl.parse("http://localhost/a/b/c/")); } @Test public void allSubPaths_whenMultipleSubPathsWithTrailingSlash_returnsExpectedUrl() { assertThat(allSubPaths("http://localhost/a/b/c/")) .containsExactly( HttpUrl.parse("http://localhost/"), HttpUrl.parse("http://localhost/a/"), HttpUrl.parse("http://localhost/a/b/"), HttpUrl.parse("http://localhost/a/b/c/")); } @Test public void allSubPaths_whenMultipleSubPathsWithParamsAndFragments_returnsExpectedUrl() { assertThat(allSubPaths("http://localhost/a/b/c/?param=value¶m2=value2#abc")) .containsExactly( HttpUrl.parse("http://localhost/"), HttpUrl.parse("http://localhost/a/"), HttpUrl.parse("http://localhost/a/b/"), HttpUrl.parse("http://localhost/a/b/c/")); } @Test public void removeLeadingSlashes_whenNoLeadingSlashes_returnsOriginal() { assertThat(removeLeadingSlashes("a/b/c/")).isEqualTo("a/b/c/"); } @Test public void removeLeadingSlashes_whenSingleLeadingSlash_removesLeadingSlashes() { assertThat(removeLeadingSlashes("/a/b/c/")).isEqualTo("a/b/c/"); } @Test public void removeLeadingSlashes_whenMultipleLeadingSlashes_removesLeadingSlashes() { assertThat(removeLeadingSlashes("/////a/b/c/")).isEqualTo("a/b/c/"); } @Test public void removeTrailingSlashes_whenNoTrailingSlashes_returnsOriginal() { assertThat(removeTrailingSlashes("/a/b/c")).isEqualTo("/a/b/c"); } @Test public void removeTrailingSlashes_whenSingleTrailingSlash_removesTrailingSlashes() { assertThat(removeTrailingSlashes("/a/b/c/")).isEqualTo("/a/b/c"); } @Test public void removeTrailingSlashes_whenMultipleTrailingSlashes_removesTrailingSlashes() { assertThat(removeTrailingSlashes("/a/b/c/////")).isEqualTo("/a/b/c"); } @Test public void urlEncode_whenEmptyString_returnsOriginal() { assertThat(urlEncode("")).hasValue(""); } @Test public void urlEncode_whenNothingToEncode_returnsOriginal() { assertThat(urlEncode("abcdefghijklmnopqrstuvwxyz")).hasValue("abcdefghijklmnopqrstuvwxyz"); assertThat(urlEncode("ABCDEFGHIJKLMNOPQRSTUVWXYZ")).hasValue("ABCDEFGHIJKLMNOPQRSTUVWXYZ"); assertThat(urlEncode("0123456789")).hasValue("0123456789"); assertThat(urlEncode("-_.*")).hasValue("-_.*"); } @Test public void urlEncode_whenNotEncoded_returnsEncoded() { assertThat(urlEncode(" ")).hasValue("+"); assertThat(urlEncode("()[]{}<>")).hasValue("%28%29%5B%5D%7B%7D%3C%3E"); assertThat(urlEncode("?!@#$%^&=+,;:'\"`/\\|~")) .hasValue("%3F%21%40%23%24%25%5E%26%3D%2B%2C%3B%3A%27%22%60%2F%5C%7C%7E"); } @Test public void urlEncode_whenAlreadyEncoded_encodesAgain() { assertThat(urlEncode("%2F")).hasValue("%252F"); assertThat(urlEncode("%252F")).hasValue("%25252F"); } @Test public void urlEncode_whenComplexEncoding_encodesCorrectly() { assertThat(urlEncode("£")).hasValue("%C2%A3"); assertThat(urlEncode("つ")).hasValue("%E3%81%A4"); assertThat(urlEncode("äëïöüÿ")).hasValue("%C3%A4%C3%AB%C3%AF%C3%B6%C3%BC%C3%BF"); assertThat(urlEncode("ÄËÏÖÜŸ")).hasValue("%C3%84%C3%8B%C3%8F%C3%96%C3%9C%C5%B8"); } @Test public void urlEncode_whenUnicode_encodesOriginal() { // EURO sign assertThat(urlEncode("\u20AC")).hasValue("%E2%82%AC"); } } ================================================ FILE: common/src/test/java/com/google/tsunami/common/net/http/HttpClientModuleTest.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.net.http; import static com.google.common.truth.Truth.assertThat; import static com.google.tsunami.common.net.http.HttpRequest.get; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import com.google.inject.AbstractModule; import com.google.inject.Guice; import com.google.inject.Injector; import com.google.inject.Key; import com.google.tsunami.common.net.http.HttpClientModule.ConnectTimeout; import com.google.tsunami.common.net.http.HttpClientModule.FollowRedirects; import com.google.tsunami.common.net.http.HttpClientModule.MaxRequests; import java.io.IOException; import java.security.GeneralSecurityException; import java.security.KeyStore; import java.time.Duration; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLHandshakeException; import javax.net.ssl.SSLSocketFactory; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** Tests for {@link HttpClientModule}. */ @RunWith(JUnit4.class) public final class HttpClientModuleTest { private static final String TESTING_KEYSTORE = "testdata/tsunami_test_server.p12"; private static final char[] TESTING_KEYSTORE_PASSWORD = "tsunamitest".toCharArray(); private final HttpClientCliOptions cliOptions = new HttpClientCliOptions(); private final HttpClientConfigProperties configProperties = new HttpClientConfigProperties(); @Test public void provideHttpClient_always_createsSingleton() { Injector injector = Guice.createInjector(new HttpClientModule.Builder().setMaxRequests(10).build()); HttpClient httpClient = injector.getInstance(HttpClient.class); HttpClient httpClient2 = injector.getInstance(HttpClient.class); assertThat(httpClient).isSameInstanceAs(httpClient2); } @Test public void setConnectionPoolMaxIdle_whenNonPositiveMaxIdle_throwsIllegalArgumentException() { HttpClientModule.Builder builder = new HttpClientModule.Builder(); assertThrows(IllegalArgumentException.class, () -> builder.setConnectionPoolMaxIdle(-1)); assertThrows(IllegalArgumentException.class, () -> builder.setConnectionPoolMaxIdle(0)); } @Test public void setConnectionPoolKeepAliveDuration_whenNegativeDuration_throwsIllegalArgumentException() { HttpClientModule.Builder builder = new HttpClientModule.Builder(); assertThrows( IllegalArgumentException.class, () -> builder.setConnectionPoolKeepAliveDuration(Duration.ofMillis(-1))); } @Test public void setMaxRequests_whenPositiveRequests_setsValueToDispatcher() { Injector injector = Guice.createInjector(new HttpClientModule.Builder().setMaxRequests(10).build()); assertThat(injector.getInstance(Key.get(int.class, MaxRequests.class))).isEqualTo(10); } @Test public void setMaxRequests_whenNonPositiveRequests_throwsIllegalArgumentException() { HttpClientModule.Builder builder = new HttpClientModule.Builder(); assertThrows(IllegalArgumentException.class, () -> builder.setMaxRequests(-1)); assertThrows(IllegalArgumentException.class, () -> builder.setMaxRequests(0)); } @Test public void setFollowRedirects_always_setsValueToClient() { Injector injector = Guice.createInjector(new HttpClientModule.Builder().setFollowRedirects(true).build()); assertTrue(injector.getInstance(Key.get(Boolean.class, FollowRedirects.class))); } @Test public void setTrustAllCertificates_whenFalseAndCertIsInvalid_throws() throws GeneralSecurityException, IOException { cliOptions.trustAllCertificates = false; configProperties.trustAllCertificates = false; HttpClient httpClient = Guice.createInjector(getTestingGuiceModuleWithConfigs()).getInstance(HttpClient.class); MockWebServer mockWebServer = startMockWebServerWithSsl(); // The certificate used in test is a self-signed one. HttpClient will reject it unless the // certificate is explicitly trusted. assertThrows( SSLHandshakeException.class, () -> httpClient.send(get(mockWebServer.url("/")).withEmptyHeaders().build())); // Note: b/314642696 - After this point, the socket in mockWebServer was closed when the // exception was raised. So working with the mockWebServer would be // hazardous. } @Test public void setTrustAllCertificates_whenBothCliAndConfigValuesAreSet_cliValueTakesPrecedence() throws GeneralSecurityException, IOException { cliOptions.trustAllCertificates = false; configProperties.trustAllCertificates = true; HttpClient httpClient = Guice.createInjector(getTestingGuiceModuleWithConfigs()).getInstance(HttpClient.class); MockWebServer mockWebServer = startMockWebServerWithSsl(); // The certificate used in test is a self-signed one. HttpClient will reject it unless the // certificate is explicitly trusted. assertThrows( SSLHandshakeException.class, () -> httpClient.send(get(mockWebServer.url("/")).withEmptyHeaders().build())); // Note: b/314642696 - After this point, the socket in mockWebServer was closed when the // exception was raised. So working with the mockWebServer would be // hazardous. } @Test public void setTrustAllCertificates_whenCliOptionEnabledAndCertIsInvalid_ignoresCertError() throws GeneralSecurityException, IOException { cliOptions.trustAllCertificates = true; HttpClient httpClient = Guice.createInjector(getTestingGuiceModuleWithConfigs()).getInstance(HttpClient.class); MockWebServer mockWebServer = startMockWebServerWithSsl(); HttpResponse response = httpClient.send(get(mockWebServer.url("/")).withEmptyHeaders().build()); assertThat(response.bodyString()).hasValue("body"); mockWebServer.shutdown(); } @Test public void setTrustAllCertificates_whenConfigPropropertyEnabledAndCertIsInvalid_ignoresCertError() throws GeneralSecurityException, IOException { configProperties.trustAllCertificates = true; HttpClient httpClient = Guice.createInjector(getTestingGuiceModuleWithConfigs()).getInstance(HttpClient.class); MockWebServer mockWebServer = startMockWebServerWithSsl(); HttpResponse response = httpClient.send(get(mockWebServer.url("/")).withEmptyHeaders().build()); assertThat(response.bodyString()).hasValue("body"); mockWebServer.shutdown(); } @Test public void setConnectTimeoutSeconds_whenSpecifiedUsingCliOptions_setsValueFromCli() { cliOptions.connectTimeoutSeconds = 50; Injector injector = Guice.createInjector(getTestingGuiceModuleWithConfigs()); assertThat(injector.getInstance(Key.get(Duration.class, ConnectTimeout.class))) .isEqualTo(Duration.ofSeconds(50)); } @Test public void setConnectTimeoutSeconds_whenSpecifiedUsingConfigProperties_setsValueFromConfig() { configProperties.connectTimeoutSeconds = 50; Injector injector = Guice.createInjector(getTestingGuiceModuleWithConfigs()); assertThat(injector.getInstance(Key.get(Duration.class, ConnectTimeout.class))) .isEqualTo(Duration.ofSeconds(50)); } @Test public void setConnectTimeoutSeconds_whenBothCliAndConfigAreSet_cliTakesPrecedence() { cliOptions.connectTimeoutSeconds = 50; configProperties.connectTimeoutSeconds = 30; Injector injector = Guice.createInjector(getTestingGuiceModuleWithConfigs()); assertThat(injector.getInstance(Key.get(Duration.class, ConnectTimeout.class))) .isEqualTo(Duration.ofSeconds(50)); } @Test public void setConnectTimeoutSeconds_whenBothCliAndConfigAreNotSet_setsDefaultValue() { Injector injector = Guice.createInjector(getTestingGuiceModuleWithConfigs()); assertThat(injector.getInstance(Key.get(Duration.class, ConnectTimeout.class))) .isEqualTo(Duration.ofSeconds(10)); } private AbstractModule getTestingGuiceModuleWithConfigs() { return new AbstractModule() { @Override protected void configure() { install(new HttpClientModule.Builder().build()); bind(HttpClientCliOptions.class).toInstance(cliOptions); bind(HttpClientConfigProperties.class).toInstance(configProperties); } }; } private MockWebServer startMockWebServerWithSsl() throws GeneralSecurityException, IOException { MockWebServer mockWebServer = new MockWebServer(); mockWebServer.useHttps(getTestingSslSocketFactory(), false); mockWebServer.enqueue(new MockResponse().setResponseCode(HttpStatus.OK.code()).setBody("body")); mockWebServer.start(); return mockWebServer; } private SSLSocketFactory getTestingSslSocketFactory() throws GeneralSecurityException, IOException { final KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); KeyStore keyStore = KeyStore.getInstance("PKCS12"); keyStore.load(getClass().getResourceAsStream(TESTING_KEYSTORE), TESTING_KEYSTORE_PASSWORD); keyManagerFactory.init(keyStore, TESTING_KEYSTORE_PASSWORD); SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(keyManagerFactory.getKeyManagers(), null, null); return sslContext.getSocketFactory(); } } ================================================ FILE: common/src/test/java/com/google/tsunami/common/net/http/HttpHeadersTest.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.net.http; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; import com.google.common.collect.ImmutableListMultimap; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** Tests for {@link HttpHeaders}. */ @RunWith(JUnit4.class) public class HttpHeadersTest { @Test public void builderAddHeader_always_putsInHeadersMap() { HttpHeaders httpHeaders = HttpHeaders.builder().addHeader("test_header", "test_value").build(); assertThat(httpHeaders.rawHeaders()) .containsExactlyEntriesIn(ImmutableListMultimap.of("test_header", "test_value")); } @Test public void builderAddHeader_withKnownHeader_canonicalizesHeaderName() { HttpHeaders httpHeaders = HttpHeaders.builder() .addHeader(com.google.common.net.HttpHeaders.ACCEPT.toUpperCase(), "test_value") .build(); assertThat(httpHeaders.rawHeaders()) .containsExactlyEntriesIn( ImmutableListMultimap.of(com.google.common.net.HttpHeaders.ACCEPT, "test_value")); } @Test public void builderAddHeader_whenEnableCanonicalization_canonicalizesHeaderName() { HttpHeaders httpHeaders = HttpHeaders.builder() .addHeader("TEST_Header", "test_value", true) .build(); assertThat(httpHeaders.rawHeaders()) .containsExactlyEntriesIn( ImmutableListMultimap.of("test_header", "test_value")); } @Test public void builderAddHeader_whenDisableCanonicalization_addsHeaderNameAsIs() { HttpHeaders httpHeaders = HttpHeaders.builder() .addHeader("TEST_Header", "test_value", false) .build(); assertThat(httpHeaders.rawHeaders()) .containsExactlyEntriesIn( ImmutableListMultimap.of("TEST_Header", "test_value")); } @Test public void builderAddHeader_withNullName_throwsNullPointerException() { assertThrows( NullPointerException.class, () -> HttpHeaders.builder().addHeader(null, "test_value")); } @Test public void builderAddHeader_withNullValue_throwsNullPointerException() { assertThrows( NullPointerException.class, () -> HttpHeaders.builder().addHeader("test_header", null)); } @Test public void builderAddHeader_withIllegalHeaderName_throwsIllegalArgumentException() { assertThrows( IllegalArgumentException.class, () -> HttpHeaders.builder().addHeader(":::", "test_value")); } @Test public void builderAddHeader_withIllegalHeaderValue_throwsIllegalArgumentException() { assertThrows( IllegalArgumentException.class, () -> HttpHeaders.builder().addHeader("test_header", String.valueOf((char) 11))); } @Test public void names_always_returnsAllHeaderNames() { HttpHeaders httpHeaders = HttpHeaders.builder() .addHeader(com.google.common.net.HttpHeaders.ACCEPT, "*/*") .addHeader(com.google.common.net.HttpHeaders.CONTENT_TYPE, "text/html; charset=UTF-8") .addHeader(com.google.common.net.HttpHeaders.ACCEPT, "text/html") .build(); assertThat(httpHeaders.names()) .containsExactly( com.google.common.net.HttpHeaders.ACCEPT, com.google.common.net.HttpHeaders.CONTENT_TYPE); } @Test public void get_whenRequestedHeaderExists_returnsRequestedHeader() { HttpHeaders httpHeaders = HttpHeaders.builder() .addHeader(com.google.common.net.HttpHeaders.ACCEPT, "*/*") .addHeader(com.google.common.net.HttpHeaders.CONTENT_TYPE, "text/html; charset=UTF-8") .build(); assertThat(httpHeaders.get(com.google.common.net.HttpHeaders.ACCEPT)).hasValue("*/*"); } @Test public void get_whenMultipleValuesExist_returnsFirstValue() { HttpHeaders httpHeaders = HttpHeaders.builder() .addHeader(com.google.common.net.HttpHeaders.ACCEPT, "*/*") .addHeader(com.google.common.net.HttpHeaders.CONTENT_TYPE, "text/html; charset=UTF-8") .addHeader(com.google.common.net.HttpHeaders.ACCEPT, "text/html") .build(); assertThat(httpHeaders.get(com.google.common.net.HttpHeaders.ACCEPT)).hasValue("*/*"); } @Test public void get_whenRequestedHeaderDoesNotExist_returnsEmpty() { HttpHeaders httpHeaders = HttpHeaders.builder() .addHeader(com.google.common.net.HttpHeaders.ACCEPT, "*/*") .addHeader(com.google.common.net.HttpHeaders.CONTENT_TYPE, "text/html; charset=UTF-8") .addHeader(com.google.common.net.HttpHeaders.ACCEPT, "text/html") .build(); assertThat(httpHeaders.get(com.google.common.net.HttpHeaders.COOKIE)).isEmpty(); } @Test public void get_withNullHeaderName_throwsNullPointerException() { HttpHeaders httpHeaders = HttpHeaders.builder() .addHeader(com.google.common.net.HttpHeaders.ACCEPT, "*/*") .addHeader(com.google.common.net.HttpHeaders.CONTENT_TYPE, "text/html; charset=UTF-8") .addHeader(com.google.common.net.HttpHeaders.ACCEPT, "text/html") .build(); assertThrows(NullPointerException.class, () -> httpHeaders.get(null)); } @Test public void getAll_always_returnsAllRequestedValues() { HttpHeaders httpHeaders = HttpHeaders.builder() .addHeader(com.google.common.net.HttpHeaders.ACCEPT, "*/*") .addHeader(com.google.common.net.HttpHeaders.CONTENT_TYPE, "text/html; charset=UTF-8") .addHeader(com.google.common.net.HttpHeaders.ACCEPT, "text/html") .build(); assertThat(httpHeaders.getAll(com.google.common.net.HttpHeaders.ACCEPT)) .containsExactly("*/*", "text/html"); } @Test public void getAll_withKnownHeaderValue_canonicalizesRequestedHeader() { HttpHeaders httpHeaders = HttpHeaders.builder() .addHeader(com.google.common.net.HttpHeaders.ACCEPT, "*/*") .addHeader(com.google.common.net.HttpHeaders.CONTENT_TYPE, "text/html; charset=UTF-8") .addHeader(com.google.common.net.HttpHeaders.ACCEPT, "text/html") .build(); assertThat(httpHeaders.getAll(com.google.common.net.HttpHeaders.ACCEPT.toUpperCase())) .containsExactly("*/*", "text/html"); } @Test public void getAll_whenRequestValueDoesNotExist_returnsEmptyList() { HttpHeaders httpHeaders = HttpHeaders.builder() .addHeader(com.google.common.net.HttpHeaders.ACCEPT, "*/*") .addHeader(com.google.common.net.HttpHeaders.CONTENT_TYPE, "text/html; charset=UTF-8") .addHeader(com.google.common.net.HttpHeaders.ACCEPT, "text/html") .build(); assertThat(httpHeaders.getAll(com.google.common.net.HttpHeaders.COOKIE)).isEmpty(); } } ================================================ FILE: common/src/test/java/com/google/tsunami/common/net/http/HttpRequestTest.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.net.http; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; import com.google.protobuf.ByteString; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** Tests for {@link HttpRequest}. */ @RunWith(JUnit4.class) public class HttpRequestTest { @Test public void get_always_buildsHttpGetRequest() { HttpRequest httpRequest = HttpRequest.get("http://localhost/url").withEmptyHeaders().build(); assertThat(httpRequest.method()).isEqualTo(HttpMethod.GET); assertThat(httpRequest.url()).isEqualTo("http://localhost/url"); } @Test public void head_always_buildsHttpHeadRequest() { HttpRequest httpRequest = HttpRequest.head("http://localhost/url").withEmptyHeaders().build(); assertThat(httpRequest.method()).isEqualTo(HttpMethod.HEAD); assertThat(httpRequest.url()).isEqualTo("http://localhost/url"); } @Test public void post_always_buildsHttpPostRequest() { HttpRequest httpRequest = HttpRequest.post("http://localhost/url").withEmptyHeaders().build(); assertThat(httpRequest.method()).isEqualTo(HttpMethod.POST); assertThat(httpRequest.url()).isEqualTo("http://localhost/url"); } @Test public void delete_always_buildsHttpDeleteRequest() { HttpRequest httpRequest = HttpRequest.delete("http://localhost/url").withEmptyHeaders().build(); assertThat(httpRequest.method()).isEqualTo(HttpMethod.DELETE); assertThat(httpRequest.url()).isEqualTo("http://localhost/url"); } @Test public void build_whenGetRequestHasRequestBody_throwsIllegalStateException() { assertThrows( IllegalStateException.class, () -> HttpRequest.builder() .setMethod(HttpMethod.GET) .setUrl("http://localhost") .setHeaders(HttpHeaders.builder().build()) .setRequestBody(ByteString.EMPTY) .build()); } } ================================================ FILE: common/src/test/java/com/google/tsunami/common/net/http/HttpResponseTest.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.net.http; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import com.google.protobuf.ByteString; import okhttp3.HttpUrl; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** Tests for {@link HttpResponse}. */ @RunWith(JUnit4.class) public final class HttpResponseTest { private static final HttpUrl TEST_URL = HttpUrl.parse("https://example.com/"); @Test public void bodyJson_whenValidResponseBody_returnsParsedJson() { HttpResponse httpResponse = HttpResponse.builder() .setStatus(HttpStatus.OK) .setHeaders(HttpHeaders.builder().build()) .setBodyBytes(ByteString.copyFromUtf8("{ \"test_value\": 1 }")) .setResponseUrl(TEST_URL) .build(); assertThat(httpResponse.bodyJson()).isPresent(); assertThat(httpResponse.bodyJson().get().isJsonObject()).isTrue(); assertThat( httpResponse .bodyJson() .get() .getAsJsonObject() .getAsJsonPrimitive("test_value") .getAsInt()) .isEqualTo(1); } @Test public void bodyJson_whenEmptyResponseBody_returnsEmptyOptional() { HttpResponse httpResponse = HttpResponse.builder() .setStatus(HttpStatus.OK) .setHeaders(HttpHeaders.builder().build()) .setResponseUrl(TEST_URL) .build(); assertThat(httpResponse.bodyJson()).isEmpty(); } @Test public void bodyJson_whenNonJsonResponseBody_returnsEmptyOptional() { HttpResponse httpResponse = HttpResponse.builder() .setStatus(HttpStatus.OK) .setHeaders(HttpHeaders.builder().build()) .setBodyBytes(ByteString.copyFromUtf8("not a json")) .setResponseUrl(TEST_URL) .build(); assertThat(httpResponse.bodyJson()).isEmpty(); } @Test public void bodyJson_whenEmptyBodyResponseBody_throwsJsonSyntaxException() { HttpResponse httpResponse = HttpResponse.builder() .setStatus(HttpStatus.OK) .setHeaders(HttpHeaders.builder().build()) .setBodyBytes(ByteString.copyFromUtf8("")) .setResponseUrl(TEST_URL) .build(); assertThrows( IllegalStateException.class, () -> httpResponse.jsonFieldEqualsToValue("field", "value")); } @Test public void jsonFieldEqualsToValue_whenEmptyJsonResponseBody_returnsFalse() { HttpResponse httpResponse = HttpResponse.builder() .setStatus(HttpStatus.OK) .setHeaders(HttpHeaders.builder().build()) .setBodyBytes(ByteString.copyFromUtf8("{}")) .setResponseUrl(TEST_URL) .build(); assertFalse(httpResponse.jsonFieldEqualsToValue("field", "value")); } @Test public void jsonFieldEqualsToValue_whenNonJsonResponseBody_returnsEmptyOptional() { HttpResponse httpResponse = HttpResponse.builder() .setStatus(HttpStatus.OK) .setHeaders(HttpHeaders.builder().build()) .setBodyBytes(ByteString.copyFromUtf8("not a json")) .setResponseUrl(TEST_URL) .build(); assertThat(httpResponse.bodyJson()).isEmpty(); } @Test public void jsonFieldEqualsToValue_whenJsonFieldContainsValue_returnsTrue() { HttpResponse httpResponse = HttpResponse.builder() .setStatus(HttpStatus.OK) .setHeaders(HttpHeaders.builder().build()) .setBodyBytes(ByteString.copyFromUtf8("{\"field\": \"value\"}")) .setResponseUrl(TEST_URL) .build(); assertTrue(httpResponse.jsonFieldEqualsToValue("field", "value")); } @Test public void bodyJson_whenHttpStatusInvalid_parseSucceeds() { HttpResponse httpResponse = HttpResponse.builder() .setStatus(HttpStatus.HTTP_STATUS_UNSPECIFIED) .setHeaders(HttpHeaders.builder().build()) .setResponseUrl(TEST_URL) .build(); assertFalse(httpResponse.status().isSuccess()); } } ================================================ FILE: common/src/test/java/com/google/tsunami/common/net/http/OkHttpHttpClientTest.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.net.http; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Strings.nullToEmpty; import static com.google.common.net.HttpHeaders.ACCEPT; import static com.google.common.net.HttpHeaders.CONTENT_LENGTH; import static com.google.common.net.HttpHeaders.CONTENT_TYPE; import static com.google.common.net.HttpHeaders.HOST; import static com.google.common.net.HttpHeaders.LOCATION; import static com.google.common.net.HttpHeaders.USER_AGENT; import static com.google.common.truth.Truth.assertThat; import static com.google.tsunami.common.net.http.HttpRequest.get; import static com.google.tsunami.common.net.http.HttpRequest.head; import static com.google.tsunami.common.net.http.HttpRequest.post; import static java.nio.charset.StandardCharsets.UTF_8; import static org.junit.Assert.assertThrows; import com.google.common.net.MediaType; import com.google.common.util.concurrent.ListenableFuture; import com.google.inject.AbstractModule; import com.google.inject.Guice; import com.google.protobuf.ByteString; import com.google.tsunami.common.data.NetworkEndpointUtils; import com.google.tsunami.proto.NetworkService; import java.io.IOException; import java.net.InetAddress; import java.net.URL; import java.security.GeneralSecurityException; import java.security.KeyStore; import java.util.Optional; import java.util.concurrent.ExecutionException; import javax.inject.Inject; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLException; import javax.net.ssl.SSLSocketFactory; import okhttp3.HttpUrl; import okhttp3.mockwebserver.Dispatcher; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import okhttp3.mockwebserver.RecordedRequest; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** Tests for {@link OkHttpHttpClient}. */ @RunWith(JUnit4.class) public final class OkHttpHttpClientTest { private static final String TESTING_KEYSTORE = "testdata/tsunami_test_server.p12"; private static final char[] TESTING_KEYSTORE_PASSWORD = "tsunamitest".toCharArray(); private MockWebServer mockWebServer; @Inject private HttpClient httpClient; @Before public void setUp() { mockWebServer = new MockWebServer(); Guice.createInjector(new HttpClientModule.Builder().build()).injectMembers(this); } @After public void tearDown() throws IOException { mockWebServer.shutdown(); } @Test public void sendAsIs_always_returnsExpectedHttpResponse() throws IOException, InterruptedException { mockWebServer.setDispatcher(new SendAsIsTestDispatcher()); mockWebServer.start(); String expectedResponseBody = SendAsIsTestDispatcher.buildBody("GET", ""); HttpUrl baseUrl = mockWebServer.url("/"); String requestUrl = new URL( baseUrl.scheme(), baseUrl.host(), baseUrl.port(), "/send-as-is/%2e%2e/%2e%2e/etc/passwd") .toString(); HttpResponse response = httpClient.sendAsIs(get(requestUrl).withEmptyHeaders().build()); assertThat(mockWebServer.takeRequest().getPath()) .isEqualTo("/send-as-is/%2e%2e/%2e%2e/etc/passwd"); assertThat(response) .isEqualTo( HttpResponse.builder() .setStatus(HttpStatus.OK) .setHeaders( HttpHeaders.builder() .addHeader(CONTENT_TYPE, MediaType.PLAIN_TEXT_UTF_8.toString()) // MockWebServer always adds this response header. .addHeader(CONTENT_LENGTH, String.valueOf(expectedResponseBody.length())) .build()) .setBodyBytes(ByteString.copyFrom(expectedResponseBody, UTF_8)) .build()); } @Test public void sendAsIs_withPostRequest_returnsExpectedHttpResponse() throws IOException, InterruptedException { mockWebServer.setDispatcher(new SendAsIsTestDispatcher()); mockWebServer.start(); String requestBody = "POST BODY"; String expectedResponseBody = SendAsIsTestDispatcher.buildBody("POST", requestBody); HttpUrl baseUrl = mockWebServer.url("/"); String requestUrl = new URL(baseUrl.scheme(), baseUrl.host(), baseUrl.port(), "/send-as-is/%2e%2e/%2e%2e/path") .toString(); HttpResponse response = httpClient.sendAsIs( post(requestUrl) .setRequestBody(ByteString.copyFrom(requestBody, UTF_8)) .withEmptyHeaders() .build()); assertThat(mockWebServer.takeRequest().getPath()).isEqualTo("/send-as-is/%2e%2e/%2e%2e/path"); assertThat(response) .isEqualTo( HttpResponse.builder() .setStatus(HttpStatus.OK) .setHeaders( HttpHeaders.builder() .addHeader(CONTENT_TYPE, MediaType.PLAIN_TEXT_UTF_8.toString()) // MockWebServer always adds this response header. .addHeader(CONTENT_LENGTH, String.valueOf(expectedResponseBody.length())) .build()) .setBodyBytes(ByteString.copyFrom(expectedResponseBody, UTF_8)) .build()); } @Test public void send_always_canonicalizesRequestUrl() throws IOException, InterruptedException { String responseBody = "test response"; mockWebServer.enqueue( new MockResponse() .setResponseCode(HttpStatus.OK.code()) .setHeader(CONTENT_TYPE, MediaType.PLAIN_TEXT_UTF_8.toString()) .setBody(responseBody)); mockWebServer.start(); HttpUrl baseUrl = mockWebServer.url("/"); String requestUrl = new URL(baseUrl.scheme(), baseUrl.host(), baseUrl.port(), "/%2e%2e/%2e%2e/etc/passwd") .toString(); httpClient.send(get(requestUrl).withEmptyHeaders().build()); assertThat(mockWebServer.takeRequest().getPath()).isEqualTo("/etc/passwd"); } @Test public void send_whenGetRequest_returnsExpectedHttpResponse() throws IOException { String responseBody = "test response"; mockWebServer.enqueue( new MockResponse() .setResponseCode(HttpStatus.OK.code()) .setHeader(CONTENT_TYPE, MediaType.PLAIN_TEXT_UTF_8.toString()) .setBody(responseBody)); mockWebServer.start(); String requestUrl = mockWebServer.url("/test/get").toString(); HttpResponse response = httpClient.send(get(requestUrl).withEmptyHeaders().build()); assertThat(response) .isEqualTo( HttpResponse.builder() .setStatus(HttpStatus.OK) .setHeaders( HttpHeaders.builder() .addHeader(CONTENT_TYPE, MediaType.PLAIN_TEXT_UTF_8.toString()) // MockWebServer always adds this response header. .addHeader(CONTENT_LENGTH, String.valueOf(responseBody.length())) .build()) .setBodyBytes(ByteString.copyFrom(responseBody, UTF_8)) .setResponseUrl(HttpUrl.parse(requestUrl)) .build()); } @Test public void sendAsync_whenGetRequest_returnsExpectedHttpResponse() throws IOException, ExecutionException, InterruptedException { String responseBody = "test response"; mockWebServer.enqueue( new MockResponse() .setResponseCode(HttpStatus.OK.code()) .setHeader(CONTENT_TYPE, MediaType.PLAIN_TEXT_UTF_8.toString()) .setBody(responseBody)); mockWebServer.start(); String requestUrl = mockWebServer.url("/test/get").toString(); HttpResponse response = httpClient.sendAsync(get(requestUrl).withEmptyHeaders().build()).get(); assertThat(response) .isEqualTo( HttpResponse.builder() .setStatus(HttpStatus.OK) .setHeaders( HttpHeaders.builder() .addHeader(CONTENT_TYPE, MediaType.PLAIN_TEXT_UTF_8.toString()) // MockWebServer always adds this response header. .addHeader(CONTENT_LENGTH, String.valueOf(responseBody.length())) .build()) .setBodyBytes(ByteString.copyFrom(responseBody, UTF_8)) .setResponseUrl(HttpUrl.parse(requestUrl)) .build()); } @Test public void send_whenHeadRequest_returnsHttpResponseWithoutBody() throws IOException { String responseBody = "test response"; mockWebServer.enqueue( new MockResponse() .setResponseCode(HttpStatus.OK.code()) .setHeader(CONTENT_TYPE, MediaType.PLAIN_TEXT_UTF_8.toString()) .setBody(responseBody)); mockWebServer.start(); String requestUrl = mockWebServer.url("/test/head").toString(); HttpResponse response = httpClient.send(head(requestUrl).withEmptyHeaders().build()); assertThat(response) .isEqualTo( HttpResponse.builder() .setStatus(HttpStatus.OK) .setHeaders( HttpHeaders.builder() .addHeader(CONTENT_TYPE, MediaType.PLAIN_TEXT_UTF_8.toString()) // MockWebServer always adds this response header. .addHeader(CONTENT_LENGTH, String.valueOf(responseBody.length())) .build()) .setBodyBytes(Optional.empty()) .setResponseUrl(HttpUrl.parse(requestUrl)) .build()); } @Test public void sendAsync_whenHeadRequest_returnsHttpResponseWithoutBody() throws IOException, ExecutionException, InterruptedException { String responseBody = "test response"; mockWebServer.enqueue( new MockResponse() .setResponseCode(HttpStatus.OK.code()) .setHeader(CONTENT_TYPE, MediaType.PLAIN_TEXT_UTF_8.toString()) .setBody(responseBody)); mockWebServer.start(); String requestUrl = mockWebServer.url("/test/head").toString(); HttpResponse response = httpClient.sendAsync(head(requestUrl).withEmptyHeaders().build()).get(); assertThat(response) .isEqualTo( HttpResponse.builder() .setStatus(HttpStatus.OK) .setHeaders( HttpHeaders.builder() .addHeader(CONTENT_TYPE, MediaType.PLAIN_TEXT_UTF_8.toString()) // MockWebServer always adds this response header. .addHeader(CONTENT_LENGTH, String.valueOf(responseBody.length())) .build()) .setBodyBytes(Optional.empty()) .setResponseUrl(HttpUrl.parse(requestUrl)) .build()); } @Test public void send_whenPostRequest_returnsExpectedHttpResponse() throws IOException { String responseBody = "{ \"test\": \"json\" }"; mockWebServer.enqueue( new MockResponse() .setResponseCode(HttpStatus.OK.code()) .setHeader(CONTENT_TYPE, MediaType.JSON_UTF_8.toString()) .setBody(responseBody)); mockWebServer.start(); String requestUrl = mockWebServer.url("/test/post").toString(); HttpResponse response = httpClient.send( post(requestUrl) .setHeaders( HttpHeaders.builder() .addHeader(ACCEPT, MediaType.JSON_UTF_8.toString()) .build()) .build()); assertThat(response) .isEqualTo( HttpResponse.builder() .setStatus(HttpStatus.OK) .setHeaders( HttpHeaders.builder() .addHeader(CONTENT_TYPE, MediaType.JSON_UTF_8.toString()) // MockWebServer always adds this response header. .addHeader(CONTENT_LENGTH, String.valueOf(responseBody.length())) .build()) .setBodyBytes(ByteString.copyFrom(responseBody, UTF_8)) .setResponseUrl(HttpUrl.parse(requestUrl)) .build()); } @Test public void sendAsync_whenPostRequest_returnsExpectedHttpResponse() throws IOException, ExecutionException, InterruptedException { String responseBody = "{ \"test\": \"json\" }"; mockWebServer.enqueue( new MockResponse() .setResponseCode(HttpStatus.OK.code()) .setHeader(CONTENT_TYPE, MediaType.JSON_UTF_8.toString()) .setBody(responseBody)); mockWebServer.start(); String requestUrl = mockWebServer.url("/test/post").toString(); HttpResponse response = httpClient .sendAsync( post(requestUrl) .setHeaders( HttpHeaders.builder() .addHeader(ACCEPT, MediaType.JSON_UTF_8.toString()) .build()) .build()) .get(); assertThat(response) .isEqualTo( HttpResponse.builder() .setStatus(HttpStatus.OK) .setHeaders( HttpHeaders.builder() .addHeader(CONTENT_TYPE, MediaType.JSON_UTF_8.toString()) // MockWebServer always adds this response header. .addHeader(CONTENT_LENGTH, String.valueOf(responseBody.length())) .build()) .setBodyBytes(ByteString.copyFrom(responseBody, UTF_8)) .setResponseUrl(HttpUrl.parse(requestUrl)) .build()); } @Test public void send_whenPostRequestWithEmptyHeaders_returnsExpectedHttpResponse() throws IOException { String responseBody = "{ \"test\": \"json\" }"; mockWebServer.enqueue( new MockResponse() .setResponseCode(HttpStatus.OK.code()) .setHeader(CONTENT_TYPE, MediaType.JSON_UTF_8.toString()) .setBody(responseBody)); mockWebServer.start(); String requestUrl = mockWebServer.url("/test/post").toString(); HttpResponse response = httpClient.send(post(requestUrl).withEmptyHeaders().build()); assertThat(response) .isEqualTo( HttpResponse.builder() .setStatus(HttpStatus.OK) .setHeaders( HttpHeaders.builder() .addHeader(CONTENT_TYPE, MediaType.JSON_UTF_8.toString()) // MockWebServer always adds this response header. .addHeader(CONTENT_LENGTH, String.valueOf(responseBody.length())) .build()) .setBodyBytes(ByteString.copyFrom(responseBody, UTF_8)) .setResponseUrl(HttpUrl.parse(requestUrl)) .build()); } @Test public void sendAsync_whenPostRequestWithEmptyHeaders_returnsExpectedHttpResponse() throws IOException, ExecutionException, InterruptedException { String responseBody = "{ \"test\": \"json\" }"; mockWebServer.enqueue( new MockResponse() .setResponseCode(HttpStatus.OK.code()) .setHeader(CONTENT_TYPE, MediaType.JSON_UTF_8.toString()) .setBody(responseBody)); mockWebServer.start(); String requestUrl = mockWebServer.url("/test/post").toString(); HttpResponse response = httpClient.sendAsync(post(requestUrl).withEmptyHeaders().build()).get(); assertThat(response) .isEqualTo( HttpResponse.builder() .setStatus(HttpStatus.OK) .setHeaders( HttpHeaders.builder() .addHeader(CONTENT_TYPE, MediaType.JSON_UTF_8.toString()) // MockWebServer always adds this response header. .addHeader(CONTENT_LENGTH, String.valueOf(responseBody.length())) .build()) .setBodyBytes(ByteString.copyFrom(responseBody, UTF_8)) .setResponseUrl(HttpUrl.parse(requestUrl)) .build()); } @Test public void send_whenFollowRedirect_returnsFinalHttpResponse() throws IOException { String responseBody = "test response"; mockWebServer.setDispatcher(new RedirectDispatcher(responseBody)); mockWebServer.start(); HttpResponse response = httpClient .modify() .setFollowRedirects(true) .build() .send( get(mockWebServer.url(RedirectDispatcher.REDIRECT_PATH).toString()) .withEmptyHeaders() .build()); HttpUrl redirectDestinationUrl = HttpUrl.parse(mockWebServer.url(RedirectDispatcher.REDIRECT_DESTINATION_PATH).toString()); assertThat(response) .isEqualTo( HttpResponse.builder() .setStatus(HttpStatus.OK) .setHeaders( HttpHeaders.builder() .addHeader(CONTENT_LENGTH, String.valueOf(responseBody.length())) .build()) .setBodyBytes(ByteString.copyFrom(responseBody, UTF_8)) .setResponseUrl(redirectDestinationUrl) .build()); } @Test public void sendAsync_whenFollowRedirect_returnsFinalHttpResponse() throws IOException, ExecutionException, InterruptedException { String responseBody = "test response"; mockWebServer.setDispatcher(new RedirectDispatcher(responseBody)); mockWebServer.start(); HttpUrl redirectDestinationUrl = HttpUrl.parse(mockWebServer.url(RedirectDispatcher.REDIRECT_DESTINATION_PATH).toString()); HttpResponse response = httpClient .modify() .setFollowRedirects(true) .build() .sendAsync( get(mockWebServer.url(RedirectDispatcher.REDIRECT_PATH).toString()) .withEmptyHeaders() .build()) .get(); assertThat(response) .isEqualTo( HttpResponse.builder() .setStatus(HttpStatus.OK) .setHeaders( HttpHeaders.builder() .addHeader(CONTENT_LENGTH, String.valueOf(responseBody.length())) .build()) .setBodyBytes(ByteString.copyFrom(responseBody, UTF_8)) .setResponseUrl(redirectDestinationUrl) .build()); } @Test public void send_whenNotFollowRedirect_returnsFinalHttpResponse() throws IOException { String responseBody = "test response"; mockWebServer.setDispatcher(new RedirectDispatcher(responseBody)); mockWebServer.start(); String redirectingUrl = mockWebServer.url(RedirectDispatcher.REDIRECT_PATH).toString(); HttpResponse response = httpClient .modify() .setFollowRedirects(false) .build() .send(get(redirectingUrl).withEmptyHeaders().build()); assertThat(response.status()).isEqualTo(HttpStatus.FOUND); assertThat(response.headers()) .isEqualTo( HttpHeaders.builder() .addHeader(CONTENT_LENGTH, "0") .addHeader(LOCATION, RedirectDispatcher.REDIRECT_DESTINATION_PATH) .build()); assertThat(response.bodyString()).hasValue(""); assertThat(response) .isEqualTo( HttpResponse.builder() .setStatus(HttpStatus.FOUND) .setHeaders( HttpHeaders.builder() .addHeader(CONTENT_LENGTH, "0") .addHeader(LOCATION, RedirectDispatcher.REDIRECT_DESTINATION_PATH) .build()) .setBodyBytes(ByteString.EMPTY) .setResponseUrl(HttpUrl.parse(redirectingUrl)) .build()); } @Test public void sendAsync_whenNotFollowRedirect_returnsFinalHttpResponse() throws IOException, ExecutionException, InterruptedException { String responseBody = "test response"; mockWebServer.setDispatcher(new RedirectDispatcher(responseBody)); mockWebServer.start(); String redirectingUrl = mockWebServer.url(RedirectDispatcher.REDIRECT_PATH).toString(); HttpResponse response = httpClient .modify() .setFollowRedirects(false) .build() .sendAsync(get(redirectingUrl).withEmptyHeaders().build()) .get(); assertThat(response.status()).isEqualTo(HttpStatus.FOUND); assertThat(response.headers()) .isEqualTo( HttpHeaders.builder() .addHeader(CONTENT_LENGTH, "0") .addHeader(LOCATION, RedirectDispatcher.REDIRECT_DESTINATION_PATH) .build()); assertThat(response.bodyString()).hasValue(""); assertThat(response) .isEqualTo( HttpResponse.builder() .setStatus(HttpStatus.FOUND) .setHeaders( HttpHeaders.builder() .addHeader(CONTENT_LENGTH, "0") .addHeader(LOCATION, RedirectDispatcher.REDIRECT_DESTINATION_PATH) .build()) .setBodyBytes(ByteString.EMPTY) .setResponseUrl(HttpUrl.parse(redirectingUrl)) .build()); } @Test public void send_whenNoUserAgentInRequest_setsCorrectUserAgentHeader() throws IOException { mockWebServer.setDispatcher(new UserAgentTestDispatcher()); mockWebServer.start(); HttpResponse response = httpClient.send( get(mockWebServer.url(UserAgentTestDispatcher.USERAGENT_TEST_PATH).toString()) .withEmptyHeaders() .build()); assertThat(response.status()).isEqualTo(HttpStatus.OK); } @Test public void send_whenUserAgentSetInRequest_overridesUserAgentHeader() throws IOException { mockWebServer.setDispatcher(new UserAgentTestDispatcher()); mockWebServer.start(); HttpResponse response = httpClient.send( get(mockWebServer.url(UserAgentTestDispatcher.USERAGENT_TEST_PATH).toString()) .setHeaders( HttpHeaders.builder().addHeader(USER_AGENT, "User Agent In Request").build()) .build()); assertThat(response.status()).isEqualTo(HttpStatus.OK); } @Test public void send_whenRequestFailed_throwsException() { assertThrows( IOException.class, () -> httpClient.send(get("http://unknownhost/path").withEmptyHeaders().build())); } @Test public void sendAsync_whenRequestFailed_returnsFutureWithException() { ListenableFuture responseFuture = httpClient.sendAsync(get("http://unknownhost/path").withEmptyHeaders().build()); ExecutionException ex = assertThrows(ExecutionException.class, responseFuture::get); assertThat(ex).hasCauseThat().isInstanceOf(IOException.class); } @Test public void send_whenHostnameAndIpInRequest_useHostnameAsProxy() throws IOException { InetAddress loopbackAddress = InetAddress.getLoopbackAddress(); String host = "host.com"; mockWebServer.setDispatcher(new HostnameTestDispatcher(host)); mockWebServer.start(loopbackAddress, 0); int port = mockWebServer.url("/").port(); NetworkService networkService = NetworkService.newBuilder() .setNetworkEndpoint( NetworkEndpointUtils.forIpHostnameAndPort( loopbackAddress.getHostAddress(), host, port)) .build(); // The request to host.com should be sent through mockWebServer's IP. HttpResponse response = httpClient.send( get(String.format("http://host.com:%d/test/get", port)).withEmptyHeaders().build(), networkService); assertThat(response.status()).isEqualTo(HttpStatus.OK); } @Test public void send_whenInvalidCertificatesAreIgnored_getResponseWithoutException() throws GeneralSecurityException, IOException { InetAddress loopbackAddress = InetAddress.getLoopbackAddress(); String host = "host.com"; MockWebServer mockWebServer = startMockWebServerWithSsl(loopbackAddress); int port = mockWebServer.url("/").port(); NetworkService networkService = NetworkService.newBuilder() .setNetworkEndpoint( NetworkEndpointUtils.forIpHostnameAndPort( loopbackAddress.getHostAddress(), host, port)) .build(); HttpClientCliOptions cliOptions = new HttpClientCliOptions(); HttpClientConfigProperties configProperties = new HttpClientConfigProperties(); cliOptions.trustAllCertificates = configProperties.trustAllCertificates = true; HttpClient httpClient = Guice.createInjector( new AbstractModule() { @Override protected void configure() { install(new HttpClientModule.Builder().build()); bind(HttpClientCliOptions.class).toInstance(cliOptions); bind(HttpClientConfigProperties.class).toInstance(configProperties); } }) .getInstance(HttpClient.class); HttpResponse response = httpClient.send( get(String.format("https://%s:%d", host, port)).withEmptyHeaders().build(), networkService); assertThat(response.bodyString()).hasValue("body"); mockWebServer.shutdown(); } @Test public void send_whenInvalidCertificatesAreNotIgnored_throws() throws GeneralSecurityException, IOException { InetAddress loopbackAddress = InetAddress.getLoopbackAddress(); String host = "host.com"; MockWebServer mockWebServer = startMockWebServerWithSsl(loopbackAddress); int port = mockWebServer.url("/").port(); NetworkService networkService = NetworkService.newBuilder() .setNetworkEndpoint( NetworkEndpointUtils.forIpHostnameAndPort( loopbackAddress.getHostAddress(), host, port)) .build(); HttpClientCliOptions cliOptions = new HttpClientCliOptions(); HttpClientConfigProperties configProperties = new HttpClientConfigProperties(); cliOptions.trustAllCertificates = configProperties.trustAllCertificates = false; HttpClient httpClient = Guice.createInjector( new AbstractModule() { @Override protected void configure() { install(new HttpClientModule.Builder().build()); bind(HttpClientCliOptions.class).toInstance(cliOptions); bind(HttpClientConfigProperties.class).toInstance(configProperties); } }) .getInstance(HttpClient.class); assertThrows( SSLException.class, () -> httpClient.send( get(String.format("https://%s:%d", host, port)).withEmptyHeaders().build(), networkService)); mockWebServer.shutdown(); } @Test public void send_default_userAgent() throws IOException, InterruptedException { String responseBody = "test response"; mockWebServer.enqueue( new MockResponse() .setResponseCode(HttpStatus.OK.code()) .setHeader(CONTENT_TYPE, MediaType.PLAIN_TEXT_UTF_8.toString()) .setBody(responseBody)); mockWebServer.start(); HttpUrl baseUrl = mockWebServer.url("/"); httpClient.send(get(baseUrl.toString()).withEmptyHeaders().build()); assertThat(mockWebServer.takeRequest().getHeader(USER_AGENT)) .isEqualTo(HttpClient.TSUNAMI_USER_AGENT); } @Test public void send_overridden_userAgent() throws IOException, InterruptedException { String responseBody = "test response"; mockWebServer.enqueue( new MockResponse() .setResponseCode(HttpStatus.OK.code()) .setHeader(CONTENT_TYPE, MediaType.PLAIN_TEXT_UTF_8.toString()) .setBody(responseBody)); mockWebServer.start(); final String userAgentOverride = "User Agent In Override"; HttpClientCliOptions cliOptions = new HttpClientCliOptions(); cliOptions.userAgent = userAgentOverride; HttpClientConfigProperties configProperties = new HttpClientConfigProperties(); cliOptions.trustAllCertificates = configProperties.trustAllCertificates = true; HttpClient httpClient = Guice.createInjector( new AbstractModule() { @Override protected void configure() { install(new HttpClientModule.Builder().build()); bind(HttpClientCliOptions.class).toInstance(cliOptions); bind(HttpClientConfigProperties.class).toInstance(configProperties); } }) .getInstance(HttpClient.class); HttpUrl baseUrl = mockWebServer.url("/"); httpClient.send(get(baseUrl.toString()).withEmptyHeaders().build()); assertThat(mockWebServer.takeRequest().getHeader(USER_AGENT)).isEqualTo(userAgentOverride); } private MockWebServer startMockWebServerWithSsl(InetAddress serverAddress) throws GeneralSecurityException, IOException { MockWebServer mockWebServer = new MockWebServer(); mockWebServer.enqueue(new MockResponse().setResponseCode(HttpStatus.OK.code()).setBody("body")); mockWebServer.useHttps(getTestingSslSocketFactory(), false); mockWebServer.start(serverAddress, 0); return mockWebServer; } private SSLSocketFactory getTestingSslSocketFactory() throws GeneralSecurityException, IOException { final KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); KeyStore keyStore = KeyStore.getInstance("PKCS12"); keyStore.load(getClass().getResourceAsStream(TESTING_KEYSTORE), TESTING_KEYSTORE_PASSWORD); keyManagerFactory.init(keyStore, TESTING_KEYSTORE_PASSWORD); SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(keyManagerFactory.getKeyManagers(), null, null); return sslContext.getSocketFactory(); } static final class RedirectDispatcher extends Dispatcher { static final String REDIRECT_PATH = "/redirect"; static final String REDIRECT_DESTINATION_PATH = "/redirect-dest"; private final String responseBody; RedirectDispatcher(String responseBody) { this.responseBody = checkNotNull(responseBody); } @Override public MockResponse dispatch(RecordedRequest recordedRequest) { switch (recordedRequest.getPath()) { case REDIRECT_PATH: return new MockResponse() .setResponseCode(HttpStatus.FOUND.code()) .setHeader(LOCATION, "/redirect-dest"); case REDIRECT_DESTINATION_PATH: return new MockResponse().setResponseCode(HttpStatus.OK.code()).setBody(responseBody); default: return new MockResponse().setResponseCode(HttpStatus.NOT_FOUND.code()); } } } static final class UserAgentTestDispatcher extends Dispatcher { static final String USERAGENT_TEST_PATH = "/useragent-test"; @Override public MockResponse dispatch(RecordedRequest recordedRequest) { if (recordedRequest.getPath().equals(USERAGENT_TEST_PATH) && nullToEmpty(recordedRequest.getHeader(USER_AGENT)).equals("TsunamiSecurityScanner")) { return new MockResponse().setResponseCode(HttpStatus.OK.code()); } return new MockResponse().setResponseCode(HttpStatus.NOT_FOUND.code()); } } static final class HostnameTestDispatcher extends Dispatcher { private final String expectedHost; HostnameTestDispatcher(String expectedHost) { this.expectedHost = checkNotNull(expectedHost); } @Override public MockResponse dispatch(RecordedRequest recordedRequest) { if (nullToEmpty(recordedRequest.getHeader(HOST)).startsWith(expectedHost)) { return new MockResponse().setResponseCode(HttpStatus.OK.code()); } return new MockResponse().setResponseCode(HttpStatus.NOT_FOUND.code()); } } static final class SendAsIsTestDispatcher extends Dispatcher { static final String SEND_AS_IS_PATH = "/send-as-is/"; static String buildBody(String method, String requestBody) { return String.format("Method: %s\nRequest Body: %s", method, requestBody); } @Override public MockResponse dispatch(RecordedRequest recordedRequest) { if (recordedRequest.getPath().startsWith(SEND_AS_IS_PATH)) { return new MockResponse() .setHeader(CONTENT_TYPE, MediaType.PLAIN_TEXT_UTF_8.toString()) .setBody(buildBody(recordedRequest.getMethod(), recordedRequest.getBody().readUtf8())) .setResponseCode(HttpStatus.OK.code()); } return new MockResponse().setResponseCode(HttpStatus.NOT_FOUND.code()); } } } ================================================ FILE: common/src/test/java/com/google/tsunami/common/net/socket/DefaultTsunamiSocketFactoryTest.java ================================================ /* * Copyright 2025 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.net.socket; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.io.IOException; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.Socket; import java.time.Duration; import javax.net.SocketFactory; import javax.net.ssl.SSLSocket; import javax.net.ssl.SSLSocketFactory; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import org.mockito.ArgumentCaptor; /** Unit tests for {@link DefaultTsunamiSocketFactory}. */ @RunWith(JUnit4.class) public final class DefaultTsunamiSocketFactoryTest { private static final Duration DEFAULT_CONNECT_TIMEOUT = Duration.ofSeconds(10); private static final Duration DEFAULT_READ_TIMEOUT = Duration.ofSeconds(30); private SocketFactory mockSocketFactory; private SSLSocketFactory mockSslSocketFactory; private Socket mockSocket; private SSLSocket mockSslSocket; private DefaultTsunamiSocketFactory tsunamiSocketFactory; @Before public void setUp() throws IOException { mockSocketFactory = mock(SocketFactory.class); mockSslSocketFactory = mock(SSLSocketFactory.class); mockSocket = mock(Socket.class); mockSslSocket = mock(SSLSocket.class); when(mockSocketFactory.createSocket()).thenReturn(mockSocket); when(mockSslSocketFactory.createSocket(any(Socket.class), anyString(), anyInt(), anyBoolean())) .thenReturn(mockSslSocket); tsunamiSocketFactory = new DefaultTsunamiSocketFactory( mockSocketFactory, mockSslSocketFactory, DEFAULT_CONNECT_TIMEOUT, DEFAULT_READ_TIMEOUT); } @Test public void constructor_withNullSocketFactory_throwsException() { assertThrows( NullPointerException.class, () -> new DefaultTsunamiSocketFactory( null, mockSslSocketFactory, DEFAULT_CONNECT_TIMEOUT, DEFAULT_READ_TIMEOUT)); } @Test public void constructor_withNullSslSocketFactory_throwsException() { assertThrows( NullPointerException.class, () -> new DefaultTsunamiSocketFactory( mockSocketFactory, null, DEFAULT_CONNECT_TIMEOUT, DEFAULT_READ_TIMEOUT)); } @Test public void constructor_withNegativeConnectTimeout_throwsException() { assertThrows( IllegalArgumentException.class, () -> new DefaultTsunamiSocketFactory( mockSocketFactory, mockSslSocketFactory, Duration.ofSeconds(-1), DEFAULT_READ_TIMEOUT)); } @Test public void constructor_withNegativeReadTimeout_throwsException() { assertThrows( IllegalArgumentException.class, () -> new DefaultTsunamiSocketFactory( mockSocketFactory, mockSslSocketFactory, DEFAULT_CONNECT_TIMEOUT, Duration.ofSeconds(-1))); } @Test public void createSocket_withHostAndPort_setsTimeoutsAndConnects() throws IOException { var unused = tsunamiSocketFactory.createSocket("example.com", 80); verify(mockSocket).setSoTimeout((int) DEFAULT_READ_TIMEOUT.toMillis()); verify(mockSocket).setKeepAlive(true); verify(mockSocket).setTcpNoDelay(true); ArgumentCaptor addressCaptor = ArgumentCaptor.forClass(InetSocketAddress.class); ArgumentCaptor timeoutCaptor = ArgumentCaptor.forClass(Integer.class); verify(mockSocket).connect(addressCaptor.capture(), timeoutCaptor.capture()); assertThat(addressCaptor.getValue().getHostString()).isEqualTo("example.com"); assertThat(addressCaptor.getValue().getPort()).isEqualTo(80); assertThat(timeoutCaptor.getValue()).isEqualTo((int) DEFAULT_CONNECT_TIMEOUT.toMillis()); } @Test public void createSocket_withCustomTimeouts_usesProvidedValues() throws IOException { Duration customConnect = Duration.ofSeconds(5); Duration customRead = Duration.ofSeconds(15); var unused = tsunamiSocketFactory.createSocket("example.com", 443, customConnect, customRead); verify(mockSocket).setSoTimeout((int) customRead.toMillis()); ArgumentCaptor timeoutCaptor = ArgumentCaptor.forClass(Integer.class); verify(mockSocket).connect(any(), timeoutCaptor.capture()); assertThat(timeoutCaptor.getValue()).isEqualTo((int) customConnect.toMillis()); } @Test public void createSocket_withInetAddress_setsTimeoutsAndConnects() throws IOException { InetAddress address = InetAddress.getLoopbackAddress(); var unused = tsunamiSocketFactory.createSocket(address, 8080); verify(mockSocket).setSoTimeout((int) DEFAULT_READ_TIMEOUT.toMillis()); verify(mockSocket).setKeepAlive(true); verify(mockSocket).setTcpNoDelay(true); ArgumentCaptor addressCaptor = ArgumentCaptor.forClass(InetSocketAddress.class); verify(mockSocket).connect(addressCaptor.capture(), anyInt()); assertThat(addressCaptor.getValue().getAddress()).isEqualTo(address); assertThat(addressCaptor.getValue().getPort()).isEqualTo(8080); } @Test public void createSocket_withInvalidPort_throwsException() { assertThrows( IllegalArgumentException.class, () -> tsunamiSocketFactory.createSocket("example.com", 0)); assertThrows( IllegalArgumentException.class, () -> tsunamiSocketFactory.createSocket("example.com", -1)); assertThrows( IllegalArgumentException.class, () -> tsunamiSocketFactory.createSocket("example.com", 65536)); } @Test public void createSocket_withNullHost_throwsException() { assertThrows( NullPointerException.class, () -> tsunamiSocketFactory.createSocket((String) null, 80)); } @Test public void createUnconnectedSocket_setsReadTimeout() throws IOException { Socket socket = tsunamiSocketFactory.createUnconnectedSocket(); assertThat(socket).isEqualTo(mockSocket); verify(mockSocket).setSoTimeout((int) DEFAULT_READ_TIMEOUT.toMillis()); } @Test public void createSslSocket_withHostAndPort_createsAndConfigures() throws IOException { var unused = tsunamiSocketFactory.createSslSocket("secure.example.com", 443); // Verify plain socket is created and connected first verify(mockSocketFactory).createSocket(); verify(mockSocket).connect(any(InetSocketAddress.class), anyInt()); // Verify SSL wrapping verify(mockSslSocketFactory).createSocket(mockSocket, "secure.example.com", 443, true); verify(mockSslSocket).setSoTimeout((int) DEFAULT_READ_TIMEOUT.toMillis()); verify(mockSslSocket).startHandshake(); } @Test public void createSslSocket_withCustomTimeouts_usesProvidedValues() throws IOException { Duration customConnect = Duration.ofSeconds(5); Duration customRead = Duration.ofSeconds(15); var unused = tsunamiSocketFactory.createSslSocket("secure.example.com", 443, customConnect, customRead); verify(mockSocket).setSoTimeout((int) customRead.toMillis()); verify(mockSslSocket).setSoTimeout((int) customRead.toMillis()); ArgumentCaptor connectTimeoutCaptor = ArgumentCaptor.forClass(Integer.class); verify(mockSocket).connect(any(), connectTimeoutCaptor.capture()); assertThat(connectTimeoutCaptor.getValue()).isEqualTo((int) customConnect.toMillis()); } @Test public void createSslSocket_withInetAddress_createsAndConfigures() throws IOException { InetAddress address = InetAddress.getLoopbackAddress(); var unused = tsunamiSocketFactory.createSslSocket(address, 443); verify(mockSocketFactory).createSocket(); verify(mockSocket).connect(any(InetSocketAddress.class), anyInt()); verify(mockSslSocketFactory).createSocket(mockSocket, address.getHostAddress(), 443, true); verify(mockSslSocket).startHandshake(); } @Test public void wrapWithSsl_wrapsExistingSocket() throws IOException { when(mockSocket.getSoTimeout()).thenReturn(5000); var unused = tsunamiSocketFactory.wrapWithSsl(mockSocket, "example.com", 443, true); verify(mockSslSocketFactory).createSocket(mockSocket, "example.com", 443, true); verify(mockSslSocket).setSoTimeout(5000); verify(mockSslSocket).startHandshake(); } @Test public void wrapWithSsl_withZeroOriginalTimeout_usesDefault() throws IOException { when(mockSocket.getSoTimeout()).thenReturn(0); var unused = tsunamiSocketFactory.wrapWithSsl(mockSocket, "example.com", 443, true); verify(mockSslSocket).setSoTimeout((int) DEFAULT_READ_TIMEOUT.toMillis()); } @Test public void wrapWithSsl_withNullSocket_throwsException() { assertThrows( NullPointerException.class, () -> tsunamiSocketFactory.wrapWithSsl(null, "example.com", 443, true)); } @Test public void wrapWithSsl_withInvalidPort_throwsException() { assertThrows( IllegalArgumentException.class, () -> tsunamiSocketFactory.wrapWithSsl(mockSocket, "example.com", 0, true)); } @Test public void getDefaultConnectTimeout_returnsConfiguredValue() { assertThat(tsunamiSocketFactory.getDefaultConnectTimeout()).isEqualTo(DEFAULT_CONNECT_TIMEOUT); } @Test public void getDefaultReadTimeout_returnsConfiguredValue() { assertThat(tsunamiSocketFactory.getDefaultReadTimeout()).isEqualTo(DEFAULT_READ_TIMEOUT); } } ================================================ FILE: common/src/test/java/com/google/tsunami/common/net/socket/TsunamiSocketFactoryCliOptionsTest.java ================================================ /* * Copyright 2025 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.net.socket; import static org.junit.Assert.assertThrows; import com.beust.jcommander.ParameterException; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** Unit tests for {@link TsunamiSocketFactoryCliOptions}. */ @RunWith(JUnit4.class) public final class TsunamiSocketFactoryCliOptionsTest { @Test public void validate_withNullValues_passes() { TsunamiSocketFactoryCliOptions options = new TsunamiSocketFactoryCliOptions(); // Should not throw options.validate(); } @Test public void validate_withPositiveConnectTimeout_passes() { TsunamiSocketFactoryCliOptions options = new TsunamiSocketFactoryCliOptions(); options.connectTimeoutSeconds = 10; // Should not throw options.validate(); } @Test public void validate_withPositiveReadTimeout_passes() { TsunamiSocketFactoryCliOptions options = new TsunamiSocketFactoryCliOptions(); options.readTimeoutSeconds = 30; // Should not throw options.validate(); } @Test public void validate_withNegativeConnectTimeout_throwsException() { TsunamiSocketFactoryCliOptions options = new TsunamiSocketFactoryCliOptions(); options.connectTimeoutSeconds = -1; assertThrows(ParameterException.class, options::validate); } @Test public void validate_withNegativeReadTimeout_throwsException() { TsunamiSocketFactoryCliOptions options = new TsunamiSocketFactoryCliOptions(); options.readTimeoutSeconds = -5; assertThrows(ParameterException.class, options::validate); } @Test public void validate_withZeroConnectTimeout_throwsException() { TsunamiSocketFactoryCliOptions options = new TsunamiSocketFactoryCliOptions(); options.connectTimeoutSeconds = 0; assertThrows(ParameterException.class, options::validate); } @Test public void validate_withZeroReadTimeout_throwsException() { TsunamiSocketFactoryCliOptions options = new TsunamiSocketFactoryCliOptions(); options.readTimeoutSeconds = 0; assertThrows(ParameterException.class, options::validate); } @Test public void validate_withTrustAllCertificates_passes() { TsunamiSocketFactoryCliOptions options = new TsunamiSocketFactoryCliOptions(); options.trustAllCertificates = true; // Should not throw options.validate(); } @Test public void validate_withAllValidOptions_passes() { TsunamiSocketFactoryCliOptions options = new TsunamiSocketFactoryCliOptions(); options.connectTimeoutSeconds = 10; options.readTimeoutSeconds = 30; options.trustAllCertificates = false; // Should not throw options.validate(); } } ================================================ FILE: common/src/test/java/com/google/tsunami/common/net/socket/TsunamiSocketFactoryModuleTest.java ================================================ /* * Copyright 2025 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.net.socket; import static com.google.common.truth.Truth.assertThat; import com.google.inject.AbstractModule; import com.google.inject.Guice; import com.google.inject.Injector; import com.google.inject.Key; import com.google.tsunami.common.net.socket.TsunamiSocketFactoryModule.ConnectTimeoutSeconds; import com.google.tsunami.common.net.socket.TsunamiSocketFactoryModule.ReadTimeoutSeconds; import com.google.tsunami.common.net.socket.TsunamiSocketFactoryModule.TrustAllCertificates; import java.time.Duration; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** Unit tests for {@link TsunamiSocketFactoryModule}. */ @RunWith(JUnit4.class) public final class TsunamiSocketFactoryModuleTest { private TsunamiSocketFactoryCliOptions cliOptions; private TsunamiSocketFactoryConfigProperties configProperties; @Before public void setUp() { cliOptions = new TsunamiSocketFactoryCliOptions(); configProperties = new TsunamiSocketFactoryConfigProperties(); } @Test public void provideTsunamiSocketFactory_returnsNonNullFactory() throws Exception { Injector injector = Guice.createInjector(getTestingModule()); TsunamiSocketFactory factory = injector.getInstance(TsunamiSocketFactory.class); assertThat(factory).isNotNull(); assertThat(factory).isInstanceOf(DefaultTsunamiSocketFactory.class); } @Test public void provideTsunamiSocketFactory_withDefaultConfig_usesDefaultTimeouts() throws Exception { Injector injector = Guice.createInjector(getTestingModule()); TsunamiSocketFactory factory = injector.getInstance(TsunamiSocketFactory.class); assertThat(factory.getDefaultConnectTimeout()).isEqualTo(Duration.ofSeconds(10)); assertThat(factory.getDefaultReadTimeout()).isEqualTo(Duration.ofSeconds(30)); } @Test public void provideConnectTimeoutSeconds_withCliOption_usesCliValue() { cliOptions.connectTimeoutSeconds = 20; configProperties.connectTimeoutSeconds = 15; Injector injector = Guice.createInjector(getTestingModule()); assertThat(injector.getInstance(Key.get(int.class, ConnectTimeoutSeconds.class))).isEqualTo(20); } @Test public void provideConnectTimeoutSeconds_withConfigOnly_usesConfigValue() { configProperties.connectTimeoutSeconds = 15; Injector injector = Guice.createInjector(getTestingModule()); assertThat(injector.getInstance(Key.get(int.class, ConnectTimeoutSeconds.class))).isEqualTo(15); } @Test public void provideConnectTimeoutSeconds_withNoConfig_usesDefault() { Injector injector = Guice.createInjector(getTestingModule()); assertThat(injector.getInstance(Key.get(int.class, ConnectTimeoutSeconds.class))).isEqualTo(10); } @Test public void provideReadTimeoutSeconds_withCliOption_usesCliValue() { cliOptions.readTimeoutSeconds = 60; configProperties.readTimeoutSeconds = 45; Injector injector = Guice.createInjector(getTestingModule()); assertThat(injector.getInstance(Key.get(int.class, ReadTimeoutSeconds.class))).isEqualTo(60); } @Test public void provideReadTimeoutSeconds_withConfigOnly_usesConfigValue() { configProperties.readTimeoutSeconds = 45; Injector injector = Guice.createInjector(getTestingModule()); assertThat(injector.getInstance(Key.get(int.class, ReadTimeoutSeconds.class))).isEqualTo(45); } @Test public void provideReadTimeoutSeconds_withNoConfig_usesDefault() { Injector injector = Guice.createInjector(getTestingModule()); assertThat(injector.getInstance(Key.get(int.class, ReadTimeoutSeconds.class))).isEqualTo(30); } @Test public void provideTrustAllCertificates_withCliOptionTrue_returnsTrue() { cliOptions.trustAllCertificates = true; configProperties.trustAllCertificates = false; Injector injector = Guice.createInjector(getTestingModule()); assertThat(injector.getInstance(Key.get(boolean.class, TrustAllCertificates.class))).isTrue(); } @Test public void provideTrustAllCertificates_withCliOptionFalse_returnsFalse() { cliOptions.trustAllCertificates = false; Injector injector = Guice.createInjector(getTestingModule()); assertThat(injector.getInstance(Key.get(boolean.class, TrustAllCertificates.class))).isFalse(); } @Test public void provideTrustAllCertificates_withConfigOnly_usesConfigValue() { configProperties.trustAllCertificates = false; Injector injector = Guice.createInjector(getTestingModule()); assertThat(injector.getInstance(Key.get(boolean.class, TrustAllCertificates.class))).isFalse(); } @Test public void provideTrustAllCertificates_withNoConfig_usesDefaultTrue() { Injector injector = Guice.createInjector(getTestingModule()); assertThat(injector.getInstance(Key.get(boolean.class, TrustAllCertificates.class))).isTrue(); } @Test public void provideTsunamiSocketFactory_withCustomConfig_usesCustomValues() throws Exception { cliOptions.connectTimeoutSeconds = 5; cliOptions.readTimeoutSeconds = 15; Injector injector = Guice.createInjector(getTestingModule()); TsunamiSocketFactory factory = injector.getInstance(TsunamiSocketFactory.class); assertThat(factory.getDefaultConnectTimeout()).isEqualTo(Duration.ofSeconds(5)); assertThat(factory.getDefaultReadTimeout()).isEqualTo(Duration.ofSeconds(15)); } private AbstractModule getTestingModule() { return new AbstractModule() { @Override protected void configure() { install(new TsunamiSocketFactoryModule()); bind(TsunamiSocketFactoryCliOptions.class).toInstance(cliOptions); bind(TsunamiSocketFactoryConfigProperties.class).toInstance(configProperties); } }; } } ================================================ FILE: common/src/test/java/com/google/tsunami/common/server/CompactRunRequestHelperTest.java ================================================ /* * Copyright 2024 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.server; import static com.google.common.truth.Truth.assertThat; import com.google.common.collect.ImmutableList; import com.google.tsunami.proto.Hostname; import com.google.tsunami.proto.MatchedPlugin; import com.google.tsunami.proto.NetworkEndpoint; import com.google.tsunami.proto.NetworkService; import com.google.tsunami.proto.PluginDefinition; import com.google.tsunami.proto.PluginInfo; import com.google.tsunami.proto.RunCompactRequest; import com.google.tsunami.proto.RunCompactRequest.PluginNetworkServiceTarget; import com.google.tsunami.proto.RunRequest; import com.google.tsunami.proto.TargetInfo; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @RunWith(JUnit4.class) public final class CompactRunRequestHelperTest { @Test public void compressingRunRequest_isMoreCompact() { NetworkService service1 = NetworkService.newBuilder().setServiceName("service1").build(); NetworkService service2 = NetworkService.newBuilder().setServiceName("service2").build(); PluginDefinition plugin1 = PluginDefinition.newBuilder() .setInfo(PluginInfo.newBuilder().setName("plugin1").build()) .build(); PluginDefinition plugin2 = PluginDefinition.newBuilder() .setInfo(PluginInfo.newBuilder().setName("plugin2").build()) .build(); PluginDefinition plugin3 = PluginDefinition.newBuilder() .setInfo(PluginInfo.newBuilder().setName("plugin3").build()) .build(); MatchedPlugin matchedPlugin1 = MatchedPlugin.newBuilder().addServices(service1).setPlugin(plugin1).build(); MatchedPlugin matchedPlugin2 = MatchedPlugin.newBuilder().addServices(service2).setPlugin(plugin2).build(); MatchedPlugin matchedPlugin3 = MatchedPlugin.newBuilder().addServices(service1).setPlugin(plugin3).build(); ImmutableList expectedMatchedPlugins = ImmutableList.of(matchedPlugin1, matchedPlugin2, matchedPlugin3); TargetInfo expectedTargetInfo = TargetInfo.newBuilder() .addNetworkEndpoints( NetworkEndpoint.newBuilder() .setHostname(Hostname.newBuilder().setName("example.com").build()) .build()) .build(); RunRequest expectedUncompressedRunRequest = RunRequest.newBuilder() .setTarget(expectedTargetInfo) .addAllPlugins(expectedMatchedPlugins) .build(); var actualCompressedRunRequest = CompactRunRequestHelper.compress(expectedUncompressedRunRequest); var expectedCompressedRunRequest = RunCompactRequest.newBuilder() .setTarget(expectedTargetInfo) .addServices(service1) .addServices(service2) .addPlugins(plugin1) .addPlugins(plugin2) .addPlugins(plugin3) .addScanTargets( PluginNetworkServiceTarget.newBuilder() .setPluginIndex(0) .setServiceIndex(0) .build()) .addScanTargets( PluginNetworkServiceTarget.newBuilder() .setPluginIndex(1) .setServiceIndex(1) .build()) .addScanTargets( PluginNetworkServiceTarget.newBuilder() .setPluginIndex(2) .setServiceIndex(0) .build()) .build(); assertThat(actualCompressedRunRequest).isEqualTo(expectedCompressedRunRequest); // And now uncompressing it again: var actualUncompressedRunRequest = CompactRunRequestHelper.uncompress(actualCompressedRunRequest); // It should match the original setup assertThat(actualUncompressedRunRequest).isEqualTo(expectedUncompressedRunRequest); } } ================================================ FILE: common/src/test/java/com/google/tsunami/common/time/SystemUtcClockModuleTest.java ================================================ /* * Copyright 2019 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.time; import static com.google.common.truth.Truth.assertThat; import com.google.inject.Guice; import com.google.inject.Key; import java.time.Clock; import java.time.ZoneOffset; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** Tests for {@link SystemUtcClockModule}. */ @RunWith(JUnit4.class) public class SystemUtcClockModuleTest { @Test public void configure_always_bindsClockToSystemUtc() { Clock clock = Guice.createInjector(new SystemUtcClockModule()) .getInstance(Key.get(Clock.class, UtcClock.class)); assertThat(clock).isNotNull(); assertThat(clock.getZone()).isEqualTo(ZoneOffset.UTC); // A hacky way of testing the instance is a SystemClock. assertThat(clock.toString()).contains("SystemClock"); } } ================================================ FILE: common/src/test/java/com/google/tsunami/common/time/testing/FakeUtcClockModuleTest.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.time.testing; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; import com.google.inject.Guice; import com.google.inject.Injector; import com.google.inject.Key; import com.google.tsunami.common.time.UtcClock; import java.time.Clock; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** Tests for {@link FakeUtcClockModule}. */ @RunWith(JUnit4.class) public class FakeUtcClockModuleTest { @Test public void constructor_withNullFakeClock_throwsNullPointerException() { assertThrows(NullPointerException.class, () -> new FakeUtcClockModule(null)); } @Test public void configure_always_bindsToSameInstance() { FakeUtcClock fakeUtcClock = FakeUtcClock.create(); Injector injector = Guice.createInjector(new FakeUtcClockModule(fakeUtcClock)); assertThat(injector.getInstance(Key.get(Clock.class, UtcClock.class))) .isSameInstanceAs(fakeUtcClock); assertThat(injector.getInstance(Key.get(Clock.class, UtcClock.class))) .isSameInstanceAs(fakeUtcClock); assertThat(injector.getInstance(Key.get(FakeUtcClock.class, UtcClock.class))) .isSameInstanceAs(fakeUtcClock); assertThat(injector.getInstance(Key.get(FakeUtcClock.class, UtcClock.class))) .isSameInstanceAs(fakeUtcClock); } } ================================================ FILE: common/src/test/java/com/google/tsunami/common/time/testing/FakeUtcClockTest.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.time.testing; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; import java.time.Duration; import java.time.Instant; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** Tests for {@link FakeUtcClock}. */ @RunWith(JUnit4.class) public class FakeUtcClockTest { private static final Instant TEST_INSTANT = Instant.ofEpochMilli(1927081738591L); private static final Duration TEST_DURATION = Duration.ofSeconds(20); @Test public void setNow_always_setsClockToGivenInstant() { assertThat(FakeUtcClock.create().setNow(TEST_INSTANT).instant()).isEqualTo(TEST_INSTANT); } @Test public void setNow_whenCallWithNull_throwsNullPointerException() { assertThrows(NullPointerException.class, () -> FakeUtcClock.create().setNow(null)); } @Test public void advance_always_advancesGivenDuration() { FakeUtcClock fakeUtcClock = FakeUtcClock.create().setNow(TEST_INSTANT); FakeUtcClock advancedClock = fakeUtcClock.advance(TEST_DURATION); assertThat(advancedClock.instant()).isEqualTo(TEST_INSTANT.plus(TEST_DURATION)); } @Test public void advance_whenCalledWithNull_throwsNullPointerException() { assertThrows(NullPointerException.class, () -> FakeUtcClock.create().advance(null)); } } ================================================ FILE: common/src/test/java/com/google/tsunami/common/version/ComparisonUtilityTest.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.version; import static com.google.common.truth.Truth.assertThat; import com.google.common.collect.Lists; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** Tests for {@link ComparisonUtility}. */ @RunWith(JUnit4.class) public final class ComparisonUtilityTest { @Test public void compareWithFillValue_bothEmptyListWithFillValueEqualToZero_returnsZero() { assertThat( ComparisonUtility.compareListWithFillValue( Lists.newArrayList(), Lists.newArrayList(), 0)) .isEqualTo(0); } @Test public void compareWithFillValue_bothEmptyListWithPositiveFillValue_returnsZero() { assertThat( ComparisonUtility.compareListWithFillValue( Lists.newArrayList(), Lists.newArrayList(), 1)) .isEqualTo(0); } @Test public void compareWithFillValue_bothEmptyListWithNegativeFilValue_returnsZero() { assertThat( ComparisonUtility.compareListWithFillValue( Lists.newArrayList(), Lists.newArrayList(), -1)) .isEqualTo(0); } @Test public void compareWithFillValue_oneEmptyListAndSmallFillValue_returnsNegative() { assertThat( ComparisonUtility.compareListWithFillValue( Lists.newArrayList(), Lists.newArrayList(1, 2, 3), 0)) .isLessThan(0); } @Test public void compareWithFillValue_oneEmptyListAndLargeFillValue_returnsPositive() { assertThat( ComparisonUtility.compareListWithFillValue( Lists.newArrayList(), Lists.newArrayList(1, 2, 3), 100)) .isGreaterThan(0); } @Test public void compareWithFillValue_nonEmptyListSameSizeGreaterValue_returnsPositive() { assertThat( ComparisonUtility.compareListWithFillValue( Lists.newArrayList(1, 3, 4), Lists.newArrayList(1, 2, 3), 100)) .isGreaterThan(0); } @Test public void compareWithFillValue_nonEmptyListSameSizeEqualValue_returnsZero() { assertThat( ComparisonUtility.compareListWithFillValue( Lists.newArrayList(1, 2, 3), Lists.newArrayList(1, 2, 3), 100)) .isEqualTo(0); } @Test public void compareWithFillValue_nonEmptyListSameSizeLessThanValue_returnsNegative() { assertThat( ComparisonUtility.compareListWithFillValue( Lists.newArrayList(1, 1, 3), Lists.newArrayList(1, 2, 3), 100)) .isLessThan(0); } @Test public void compareWithFillValue_nonEmptyListVariedSizeWithPositiveFillValue_returnsNegative() { assertThat( ComparisonUtility.compareListWithFillValue( Lists.newArrayList(1, 1), Lists.newArrayList(1, 2, 3), 100)) .isLessThan(0); } @Test public void compareWithFillValue_nonEmptyListVariedSizeWithZeroFillValue_returnsPositive() { assertThat( ComparisonUtility.compareListWithFillValue( Lists.newArrayList(1, 3), Lists.newArrayList(1, 2, 3), 0)) .isGreaterThan(0); } @Test public void compareWithFillValue_nonEmptyListVariedSizeWithZeroFillValue_returnsNegative() { assertThat( ComparisonUtility.compareListWithFillValue( Lists.newArrayList(1, 2), Lists.newArrayList(1, 2, 3), 0)) .isLessThan(0); } @Test public void compareWithFillValue_nonEmptyListVariedSizeWithPositiveFillValue_returnsPositive() { assertThat( ComparisonUtility.compareListWithFillValue( Lists.newArrayList(1, 2), Lists.newArrayList(1, 2, 3), 100)) .isGreaterThan(0); } } ================================================ FILE: common/src/test/java/com/google/tsunami/common/version/EqualsTestCase.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.version; import com.google.auto.value.AutoValue; import com.google.errorprone.annotations.Immutable; @Immutable(containerOf = "T") @AutoValue abstract class EqualsTestCase { abstract T first(); abstract T second(); static EqualsTestCase create(T first, T second) { return new AutoValue_EqualsTestCase<>(first, second); } } ================================================ FILE: common/src/test/java/com/google/tsunami/common/version/KnownQualifierTest.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.version; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; import com.google.common.collect.ImmutableList; import java.util.Arrays; import java.util.EnumSet; import org.junit.Test; import org.junit.experimental.theories.DataPoints; import org.junit.experimental.theories.FromDataPoints; import org.junit.experimental.theories.Theories; import org.junit.experimental.theories.Theory; import org.junit.runner.RunWith; /** Tests for {@link KnownQualifier}. */ @RunWith(Theories.class) public final class KnownQualifierTest { @DataPoints("ValidKnownQualifierText") public static ImmutableList validTextsForKnownQualifiers() { return Arrays.stream(KnownQualifier.values()) .map(KnownQualifier::getQualifierText) .collect(ImmutableList.toImmutableList()); } @Test public void compareTo_always_hasTheCorrectOrder() { // KnownQualifier should promise the following order to callers. assertThat(EnumSet.allOf(KnownQualifier.class)) .containsExactly( KnownQualifier.ALPHA, KnownQualifier.BETA, KnownQualifier.PRE, KnownQualifier.R, KnownQualifier.RC, KnownQualifier.ABSENT, KnownQualifier.P, KnownQualifier.PATCH, KnownQualifier.PATCHED) .inOrder(); } @Theory public void isKnownQualifier_validText_returnsTrue( @FromDataPoints("ValidKnownQualifierText") String validText) { assertThat(KnownQualifier.isKnownQualifier(validText)).isTrue(); } @Theory public void isKnownQualifier_invalidText_returnsFalse() { assertThat(KnownQualifier.isKnownQualifier("random")).isFalse(); } @Theory public void fromText_validText_returnsKnownQualifier( @FromDataPoints("ValidKnownQualifierText") String validText) { assertThat(KnownQualifier.fromText(validText)).isIn(EnumSet.allOf(KnownQualifier.class)); } @Test public void fromText_invalidText_throwsIllegalArgumentException() { assertThrows(IllegalArgumentException.class, () -> KnownQualifier.fromText("random")); } @Test public void fromText_invalidTextUsingComposedValidTokens_throwsIllegalArgumentException() { assertThrows(IllegalArgumentException.class, () -> KnownQualifier.fromText("alpha-beta")); } } ================================================ FILE: common/src/test/java/com/google/tsunami/common/version/LessThanTestCase.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.version; import com.google.auto.value.AutoValue; import com.google.errorprone.annotations.Immutable; @Immutable(containerOf = "T") @AutoValue abstract class LessThanTestCase { abstract T smaller(); abstract T larger(); static LessThanTestCase create(T smaller, T larger) { return new AutoValue_LessThanTestCase<>(smaller, larger); } } ================================================ FILE: common/src/test/java/com/google/tsunami/common/version/SegmentTest.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.version; import static com.google.common.truth.Truth.assertThat; import com.google.common.collect.ImmutableList; import java.util.stream.Collectors; import java.util.stream.Stream; import org.junit.Test; import org.junit.experimental.theories.DataPoints; import org.junit.experimental.theories.FromDataPoints; import org.junit.experimental.theories.Theories; import org.junit.experimental.theories.Theory; import org.junit.runner.RunWith; /** Tests for {@link Segment}. */ @RunWith(Theories.class) public final class SegmentTest { @Test public void fromTokenList_startsWithKnownQualifier_buildsFromInput() { ImmutableList tokens = ImmutableList.of(Token.fromKnownQualifier(KnownQualifier.ALPHA), Token.fromNumeric(1L)); Segment segment = Segment.fromTokenList(tokens); assertThat(segment.tokens()).containsExactlyElementsIn(tokens); } @Test public void fromTokenList_noKnownQualifier_addsKnownQualifier() { ImmutableList tokens = ImmutableList.of(Token.fromText("abc"), Token.fromNumeric(1L)); Segment segment = Segment.fromTokenList(tokens); assertThat(segment.tokens()) .containsExactlyElementsIn( Stream.concat( Stream.of(Token.fromKnownQualifier(KnownQualifier.ABSENT)), tokens.stream()) .collect(Collectors.toList())); } @Test public void fromString_emptyString_returnsNullSegment() { assertThat(Segment.fromString("")).isEqualTo(Segment.NULL); } @Test public void fromString_allExcludedTokens_returnsNullSegment() { assertThat(Segment.fromString("gg.N/A")).isEqualTo(Segment.NULL); } @Test public void fromString_allEmptyTokens_returnsNullSegment() { assertThat(Segment.fromString("...")).isEqualTo(Segment.NULL); } @Test public void fromString_textAndNumeric_returnsSeparatedTextAndNumber() { assertThat(Segment.fromString("abc1.0").tokens()) .containsExactly( Token.fromKnownQualifier(KnownQualifier.ABSENT), Token.fromText("abc"), Token.fromNumeric(1L), Token.fromNumeric(0L)); assertThat(Segment.fromString("gg1.0").tokens()) .containsExactly( Token.fromKnownQualifier(KnownQualifier.ABSENT), Token.fromNumeric(1L), Token.fromNumeric(0L)); } @Test public void fromString_noKnownQualifier_addsKnownQualifier() { assertThat(Segment.fromString("2.1.1").tokens()) .containsExactly( Token.fromKnownQualifier(KnownQualifier.ABSENT), Token.fromNumeric(2L), Token.fromNumeric(1L), Token.fromNumeric(1L)); } @Test public void fromString_startsWithKnownQualifier_parsesNumericAndTextTokens() { assertThat(Segment.fromString("alpha.1~text").tokens()) .containsExactly( Token.fromKnownQualifier(KnownQualifier.ALPHA), Token.fromNumeric(1L), Token.fromText("~"), Token.fromText("text")); } @DataPoints("Equals") public static ImmutableList> equalTestCases() { return ImmutableList.of( EqualsTestCase.create(Segment.fromString(""), Segment.fromString("")), EqualsTestCase.create(Segment.fromString(""), Segment.fromString("gg.N/A")), EqualsTestCase.create(Segment.fromString("1.1"), Segment.fromString("1.1")), EqualsTestCase.create(Segment.fromString("1.1-"), Segment.fromString("1.1-gg"))); } @Theory public void compareTo_equalTestCase_returnsZero( @FromDataPoints("Equals") EqualsTestCase testCase) { assertThat(testCase.first()).isEquivalentAccordingToCompareTo(testCase.second()); } @DataPoints("LessThan") public static ImmutableList> lessThanTestCases() { return ImmutableList.of( // Null token. LessThanTestCase.create(Segment.fromString("2.1"), Segment.fromString("2.1.1")), LessThanTestCase.create(Segment.fromString("alpha"), Segment.fromString("")), // Numeric token. LessThanTestCase.create(Segment.fromString("2.1"), Segment.fromString("2.2")), LessThanTestCase.create(Segment.fromString("0.9"), Segment.fromString("1.0")), // Known qualifiers. LessThanTestCase.create(Segment.fromString("alpha"), Segment.fromString("beta")), LessThanTestCase.create(Segment.fromString("alpha.beta"), Segment.fromString("alpha")), LessThanTestCase.create(Segment.fromString("alpha.beta"), Segment.fromString("alpha.rc")), // Text token. LessThanTestCase.create(Segment.fromString("abc"), Segment.fromString("def")), LessThanTestCase.create(Segment.fromString("abc.def"), Segment.fromString("abc.ghi")), LessThanTestCase.create(Segment.fromString("abc"), Segment.fromString("DEF")), // Mixed type. LessThanTestCase.create(Segment.fromString("2.1.1"), Segment.fromString("2.1.abc"))); } @Theory public void compareTo_lessThanTestCase_hasCorrectSymmetryResult( @FromDataPoints("LessThan") LessThanTestCase testCase) { assertThat(testCase.smaller()).isLessThan(testCase.larger()); assertThat(testCase.larger()).isGreaterThan(testCase.smaller()); } } ================================================ FILE: common/src/test/java/com/google/tsunami/common/version/TokenTest.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.version; import static com.google.common.truth.Truth.assertThat; import com.google.common.collect.ImmutableList; import org.junit.Test; import org.junit.experimental.theories.DataPoints; import org.junit.experimental.theories.FromDataPoints; import org.junit.experimental.theories.Theories; import org.junit.experimental.theories.Theory; import org.junit.runner.RunWith; /** Tests for {@link Token}. */ @RunWith(Theories.class) public final class TokenTest { @Test public void fromNumeric_always_returnsNumericToken() { Token numericToken = Token.fromNumeric(123L); assertThat(numericToken.isNumeric()).isTrue(); assertThat(numericToken.getNumeric()).isEqualTo(123L); } @Test public void fromText_always_returnsTextToken() { Token textToken = Token.fromText("abc"); assertThat(textToken.isText()).isTrue(); assertThat(textToken.isKnownQualifier()).isFalse(); assertThat(textToken.getText()).isEqualTo("abc"); } @Test public void fromKnownQualifier_always_returnsTextToken() { Token textToken = Token.fromKnownQualifier(KnownQualifier.ALPHA); assertThat(textToken.isText()).isTrue(); assertThat(textToken.isKnownQualifier()).isTrue(); assertThat(textToken.getText()).isEqualTo(KnownQualifier.ALPHA.getQualifierText()); } @DataPoints("Equal") public static ImmutableList> equalTestCases() { return ImmutableList.of( EqualsTestCase.create(Token.EMPTY, Token.fromKnownQualifier(KnownQualifier.ABSENT)), EqualsTestCase.create(Token.EMPTY, Token.fromText("")), EqualsTestCase.create(Token.fromNumeric(1L), Token.fromNumeric(1L)), EqualsTestCase.create( Token.fromKnownQualifier(KnownQualifier.P), Token.fromKnownQualifier(KnownQualifier.P)), EqualsTestCase.create(Token.fromKnownQualifier(KnownQualifier.P), Token.fromText("p")), EqualsTestCase.create(Token.fromKnownQualifier(KnownQualifier.P), Token.fromText("P")), EqualsTestCase.create(Token.fromText("abc"), Token.fromText("ABC"))); } @Theory public void isEqualTo_equalTestCase_returnsZero( @FromDataPoints("Equal") EqualsTestCase testCase) { assertThat(testCase.first()).isEquivalentAccordingToCompareTo(testCase.second()); } @DataPoints("LessThan") public static ImmutableList> lessThanTestCases() { return ImmutableList.of( // Empty token and Numeric token test cases. LessThanTestCase.create(Token.EMPTY, Token.fromNumeric(1L)), LessThanTestCase.create(Token.EMPTY, Token.fromNumeric(0L)), // Empty token and KnownQualifier test cases. LessThanTestCase.create(Token.fromKnownQualifier(KnownQualifier.ALPHA), Token.EMPTY), LessThanTestCase.create(Token.fromKnownQualifier(KnownQualifier.RC), Token.EMPTY), LessThanTestCase.create(Token.EMPTY, Token.fromKnownQualifier(KnownQualifier.P)), LessThanTestCase.create(Token.EMPTY, Token.fromKnownQualifier(KnownQualifier.PATCH)), // Empty token and Text token test case. LessThanTestCase.create(Token.EMPTY, Token.fromText("abc")), LessThanTestCase.create(Token.EMPTY, Token.fromText("123")), // Numeric token only test case. LessThanTestCase.create(Token.fromNumeric(0L), Token.fromNumeric(1L)), // KnownQualifier only test case. LessThanTestCase.create( Token.fromKnownQualifier(KnownQualifier.ALPHA), Token.fromKnownQualifier(KnownQualifier.ABSENT)), LessThanTestCase.create( Token.fromKnownQualifier(KnownQualifier.ABSENT), Token.fromKnownQualifier(KnownQualifier.PATCH)), // Text only test case. LessThanTestCase.create(Token.fromText("abc"), Token.fromText("def")), LessThanTestCase.create(Token.fromText("abc"), Token.fromText("dEf")), LessThanTestCase.create( Token.fromText("abc"), Token.fromKnownQualifier(KnownQualifier.ALPHA)), // Numeric and Text test case LessThanTestCase.create(Token.fromNumeric(1L), Token.fromText("abc")), LessThanTestCase.create( Token.fromNumeric(1L), Token.fromKnownQualifier(KnownQualifier.ALPHA))); } @Theory public void compareTo_lessThanTestCase_hasCorrectSymmetryResult( @FromDataPoints("LessThan") LessThanTestCase testCase) { // Test symmetry. assertThat(testCase.smaller()).isLessThan(testCase.larger()); assertThat(testCase.larger()).isGreaterThan(testCase.smaller()); } } ================================================ FILE: common/src/test/java/com/google/tsunami/common/version/VersionRangeTest.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.version; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; import com.google.tsunami.common.version.VersionRange.Inclusiveness; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** Tests for {@link VersionRange}. */ @RunWith(JUnit4.class) public class VersionRangeTest { @Test public void parse_withNegativeInfinityRange_returnsCorrectVersionRange() { VersionRange versionRange = VersionRange.parse("(,1.0]"); assertThat(versionRange) .isEqualTo( VersionRange.builder() .setMinVersion(Version.minimum()) .setMinVersionInclusiveness(Inclusiveness.EXCLUSIVE) .setMaxVersion(Version.fromString("1.0")) .setMaxVersionInclusiveness(Inclusiveness.INCLUSIVE) .build()); } @Test public void parse_withPositiveInfinityRange_returnsCorrectVersionRange() { VersionRange versionRange = VersionRange.parse("(1.0,)"); assertThat(versionRange) .isEqualTo( VersionRange.builder() .setMinVersion(Version.fromString("1.0")) .setMinVersionInclusiveness(Inclusiveness.EXCLUSIVE) .setMaxVersion(Version.maximum()) .setMaxVersionInclusiveness(Inclusiveness.EXCLUSIVE) .build()); } @Test public void parse_withRegularRange_returnsCorrectVersionRange() { VersionRange versionRange = VersionRange.parse("(1.0,2.0]"); assertThat(versionRange) .isEqualTo( VersionRange.builder() .setMinVersion(Version.fromString("1.0")) .setMinVersionInclusiveness(Inclusiveness.EXCLUSIVE) .setMaxVersion(Version.fromString("2.0")) .setMaxVersionInclusiveness(Inclusiveness.INCLUSIVE) .build()); } @Test public void parse_withEmptyRangeString_throwsIllegalArgumentException() { IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> VersionRange.parse("")); assertThat(exception).hasMessageThat().isEqualTo("Range string cannot be empty."); } @Test public void parse_withRangeNotStartingWithParenthesis_throwsIllegalArgumentException() { IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> VersionRange.parse(",1.0]")); assertThat(exception) .hasMessageThat() .isEqualTo("Version range must start with '[' or '(', got ',1.0]'"); } @Test public void parse_withRangeNotEndingWithParenthesis_throwsIllegalArgumentException() { IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> VersionRange.parse("(,1.0")); assertThat(exception) .hasMessageThat() .isEqualTo("Version range must end with ']' or ')', got '(,1.0'"); } @Test public void parse_withTooManyParenthesis_throwsIllegalArgumentException() { IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> VersionRange.parse("(,1.0]]")); assertThat(exception) .hasMessageThat() .isEqualTo("Parenthesis and/or brackets not allowed within version range, got '(,1.0]]'"); } @Test public void parse_withTooManyCommas_throwsIllegalArgumentException() { IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> VersionRange.parse("(,,,1.0]")); assertThat(exception).hasMessageThat().isEqualTo("Invalid range of versions, got '(,,,1.0]'"); } @Test public void parse_withoutComma_throwsIllegalArgumentException() { IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> VersionRange.parse("[1.0]")); assertThat(exception).hasMessageThat().isEqualTo("Invalid range of versions, got '[1.0]'"); } @Test public void parse_withMinimalToMaximalRange_throwsIllegalArgumentException() { IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> VersionRange.parse("(,)")); assertThat(exception).hasMessageThat().isEqualTo("Infinity range is not supported, got '(,)'"); } @Test public void parse_withTheSameRangeEnds_throwsIllegalArgumentException() { IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> VersionRange.parse("[1.0,1.0]")); assertThat(exception) .hasMessageThat() .isEqualTo("Min version in range must be less than max version in range, got '[1.0,1.0]'"); } } ================================================ FILE: common/src/test/java/com/google/tsunami/common/version/VersionSetTest.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.version; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; import com.google.common.collect.ImmutableList; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** Tests for {@link VersionSet}. */ @RunWith(JUnit4.class) public class VersionSetTest { @Test public void parse_withValidVersionsAndVersionRanges_returnsParsedVersionSet() { VersionSet versionSet = VersionSet.parse(ImmutableList.of("1.0", "1.3", "(1.4, 1.7]", "1.9", "[2.0,)")); assertThat(versionSet.versions()) .containsExactly( Version.fromString("1.0"), Version.fromString("1.3"), Version.fromString("1.9")); assertThat(versionSet.versionRanges()) .containsExactly(VersionRange.parse("(1.4,1.7]"), VersionRange.parse("[2.0,)")); } @Test public void parse_withEmptyInputList_throwsIllegalArgumentException() { IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> VersionSet.parse(ImmutableList.of())); assertThat(exception).hasMessageThat().isEqualTo("Versions and ranges list cannot be empty."); } @Test public void parse_withInvalidVersion_throwsIllegalArgumentException() { IllegalArgumentException exception = assertThrows( IllegalArgumentException.class, () -> VersionSet.parse(ImmutableList.of("1,0", "abc"))); assertThat(exception) .hasMessageThat() .isEqualTo("String '1,0' is neither a discrete string nor a version range."); } } ================================================ FILE: common/src/test/java/com/google/tsunami/common/version/VersionTest.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.common.version; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; import com.google.common.collect.ImmutableList; import org.junit.Test; import org.junit.experimental.theories.DataPoints; import org.junit.experimental.theories.FromDataPoints; import org.junit.experimental.theories.Theories; import org.junit.experimental.theories.Theory; import org.junit.runner.RunWith; /** Tests for {@link Version}. */ @RunWith(Theories.class) public final class VersionTest { @Test public void create_whenNormalVersionAndValueIsNull_throwsException() { assertThrows(IllegalArgumentException.class, () -> Version.fromString(null)); } @Test public void create_whnNormalVersionAndValueIsEmpty_throwsExceptionIfStringIsNull() { assertThrows(IllegalArgumentException.class, () -> Version.fromString("")); } @Test public void create_whenNormalVersion_returnsTypeNormal() { Version version = Version.fromString("1.1"); assertThat(version.versionType()).isEqualTo(Version.Type.NORMAL); } @Test public void createMaximum_always_returnsTypeMaximum() { Version version = Version.maximum(); assertThat(version.versionType()).isEqualTo(Version.Type.MAXIMUM); } @Test public void createMaximum_always_returnsTypeMinimum() { Version version = Version.minimum(); assertThat(version.versionType()).isEqualTo(Version.Type.MINIMUM); } @DataPoints("InvalidVersion") public static ImmutableList invalidVersionTestCases() { return ImmutableList.of("", "N/A", "...", "-"); } @Theory public void fromString_invalidVersionString_throwsIllegalArgumentException( @FromDataPoints("InvalidVersion") String version) { assertThrows(IllegalArgumentException.class, () -> Version.fromString(version)); } @Test public void fromString_validString_storesInputAsRawString() { assertThat(Version.fromString("1.0").versionString()).isEqualTo("1.0"); } @Test public void fromString_noEpoch_appendsZeroEpoch() { assertThat(Version.fromString("1.0").segments()) .containsExactly(Segment.fromString("0"), Segment.fromString("1.0")); } @Test public void fromString_withEpoch_epochIsParsed() { assertThat(Version.fromString("1:1.0").segments()) .containsExactly(Segment.fromString("1"), Segment.fromString("1.0")); } @Test public void fromString_withMultipleSegments_segmentsParsedCorrectly() { assertThat(Version.fromString("1:9.7.0.dfsg.P1-gg3.0").segments()) .containsExactly( Segment.fromString("1"), Segment.fromString("9.7.0.dfsg.P1"), Segment.fromString("3.0")); } @DataPoints("Equals") public static ImmutableList> equalsTestCases() { return ImmutableList.of( EqualsTestCase.create(Version.fromString("1.0"), Version.fromString("1.0")), EqualsTestCase.create(Version.fromString("1.0"), Version.fromString("0:1.0")), EqualsTestCase.create(Version.fromString("1.0-"), Version.fromString("1.0-gg")), EqualsTestCase.create(Version.maximum(), Version.maximum()), EqualsTestCase.create(Version.minimum(), Version.minimum())); } @Theory public void compareTo_equalsTestCase_returnsZero( @FromDataPoints("Equals") EqualsTestCase testCase) { assertThat(testCase.first()).isEquivalentAccordingToCompareTo(testCase.second()); } @DataPoints("LessThan") public static ImmutableList> lessThanTestCases() { return ImmutableList.of( LessThanTestCase.create(Version.minimum(), Version.fromString("1.0")), LessThanTestCase.create(Version.minimum(), Version.maximum()), LessThanTestCase.create(Version.fromString("1.0"), Version.maximum()), LessThanTestCase.create(Version.fromString("0.9"), Version.fromString("1.0")), LessThanTestCase.create(Version.fromString("1.0-0"), Version.fromString("1.0-110313082")), LessThanTestCase.create( Version.fromString("0.161-gg2.0"), Version.fromString("0.165-gg1.0")), LessThanTestCase.create(Version.fromString("0.87-gg1.2"), Version.fromString("0.87-gg1.3")), LessThanTestCase.create( Version.fromString("2017b-gg1.0"), Version.fromString("2018b-gg0.")), LessThanTestCase.create(Version.fromString("18-4"), Version.fromString("19-1")), LessThanTestCase.create( Version.fromString("1:9.7.0.dfsg.P1-gg3.0"), Version.fromString("1:9.8.0.dfsg.P0-gg1.0")), LessThanTestCase.create(Version.fromString("5.7-gg2.0"), Version.fromString("5.8-gg1.0")), LessThanTestCase.create( Version.fromString("2.4.6-12"), Version.fromString("2.4.6-12+patched1")), LessThanTestCase.create(Version.fromString("20170727-1"), Version.fromString("20170801-1")), LessThanTestCase.create( Version.fromString("3.12-443-ga51ea6dc8202-gg2.2"), Version.fromString("3.13-440-ga51eadfe472-gg1.0")), LessThanTestCase.create(Version.fromString("1.0a"), Version.fromString("1.0b")), LessThanTestCase.create(Version.fromString("1a"), Version.fromString("2")), LessThanTestCase.create( Version.fromString("4d01146f1679dd90bba45adb60d24ad11fe1155e"), Version.fromString("e40b437401966fe06b0c4d5430c35e4494675c90")), LessThanTestCase.create(Version.fromString("7.10"), Version.fromString("1_7.9")), LessThanTestCase.create(Version.fromString("9.10"), Version.fromString("1_9.11")), LessThanTestCase.create( Version.fromString("1.0.0-alpha"), Version.fromString("1.0.0-alpha.1")), LessThanTestCase.create( Version.fromString("1.0.0-alpha.1"), Version.fromString("1.0.0-alpha.beta")), LessThanTestCase.create( Version.fromString("1.0.0-alpha.beta"), Version.fromString("1.0.0-beta")), LessThanTestCase.create( Version.fromString("1.0.0-beta"), Version.fromString("1.0.0-beta.2")), LessThanTestCase.create( Version.fromString("1.0.0-beta.2"), Version.fromString("1.0.0-beta.11")), LessThanTestCase.create( Version.fromString("1.0.0-beta.11"), Version.fromString("1.0.0-rc.1")), LessThanTestCase.create(Version.fromString("1.0.0-rc.1"), Version.fromString("1.0.0")), LessThanTestCase.create(Version.fromString("1.0.0-alpha"), Version.fromString("1.0.0")), LessThanTestCase.create(Version.fromString("1.0.0-alpha.1"), Version.fromString("1.0.0")), LessThanTestCase.create( Version.fromString("1.0.0-alpha.beta"), Version.fromString("1.0.0")), LessThanTestCase.create(Version.fromString("1.0.0-beta"), Version.fromString("1.0.0")), LessThanTestCase.create(Version.fromString("1.0.0-beta.2"), Version.fromString("1.0.0")), LessThanTestCase.create(Version.fromString("1.0.0-beta.11"), Version.fromString("1.0.0")), LessThanTestCase.create(Version.fromString("7.6-0"), Version.fromString("7.6p2-4")), LessThanTestCase.create(Version.fromString("1.0-1"), Version.fromString("1.0.3-3")), LessThanTestCase.create(Version.fromString("1.2.2-2"), Version.fromString("1.3")), LessThanTestCase.create(Version.fromString("1.2.2"), Version.fromString("1.3")), LessThanTestCase.create(Version.fromString("0-pre"), Version.fromString("0-pree")), LessThanTestCase.create(Version.fromString("1.1.6r-1"), Version.fromString("1.1.6r2-2")), LessThanTestCase.create(Version.fromString("2.6b-2"), Version.fromString("2.6b2-1")), LessThanTestCase.create( Version.fromString("98.1-pre2-b6-2"), Version.fromString("98.1p5-1")), LessThanTestCase.create(Version.fromString("0.4-1"), Version.fromString("0.4a6-2")), LessThanTestCase.create(Version.fromString("1:3.0.5-2"), Version.fromString("1:3.0.5.1")), LessThanTestCase.create(Version.fromString("10.3"), Version.fromString("1:0.4")), LessThanTestCase.create(Version.fromString("1:1.25-4"), Version.fromString("1:1.25-8")), LessThanTestCase.create(Version.fromString("1.18.35"), Version.fromString("1.18.36")), LessThanTestCase.create(Version.fromString("1.18.35"), Version.fromString("0:1.18.36")), LessThanTestCase.create( Version.fromString("9:1.18.36:5.4-20"), Version.fromString("10:0.5.1-22")), LessThanTestCase.create( Version.fromString("9:1.18.36:5.4-20"), Version.fromString("9:1.18.36:5.5-1")), LessThanTestCase.create( Version.fromString("9:1.18.36:5.4-20"), Version.fromString("9:1.18.37:4.3-22")), LessThanTestCase.create( Version.fromString("1.18.36-0.17.35-18"), Version.fromString("1.18.36-19")), LessThanTestCase.create( Version.fromString("1:1.2.13-3"), Version.fromString("1:1.2.13-3.1")), LessThanTestCase.create(Version.fromString("2.0.7pre1-4"), Version.fromString("2.0.7r-1")), LessThanTestCase.create(Version.fromString("0.2"), Version.fromString("1.0-0")), LessThanTestCase.create(Version.fromString("1.0"), Version.fromString("1.0-0+b1")), LessThanTestCase.create(Version.fromString("1.2.3"), Version.fromString("1.2.3-1")), LessThanTestCase.create(Version.fromString("1.2.3"), Version.fromString("1.2.4")), LessThanTestCase.create(Version.fromString("1.2.3"), Version.fromString("1.2.4")), LessThanTestCase.create(Version.fromString("1.2.3"), Version.fromString("1.2.24")), LessThanTestCase.create(Version.fromString("0.8.7"), Version.fromString("0.10.0")), LessThanTestCase.create(Version.fromString("2.3"), Version.fromString("3.2")), LessThanTestCase.create(Version.fromString("1.3.2"), Version.fromString("1.3.2a")), LessThanTestCase.create(Version.fromString("0.5.0~git"), Version.fromString("0.5.0~git2")), LessThanTestCase.create(Version.fromString("2a"), Version.fromString("21")), LessThanTestCase.create(Version.fromString("1.3.2a"), Version.fromString("1.3.2b")), LessThanTestCase.create(Version.fromString("1.2.4"), Version.fromString("1:1.2.3")), LessThanTestCase.create(Version.fromString("1:1.2.3"), Version.fromString("1:1.2.4")), LessThanTestCase.create(Version.fromString("1.2a+~bCd3"), Version.fromString("1.2a++")), LessThanTestCase.create(Version.fromString("1.2a+~"), Version.fromString("1.2a+~bCd3")), LessThanTestCase.create(Version.fromString("304-2"), Version.fromString("5:2")), LessThanTestCase.create(Version.fromString("5:2"), Version.fromString("304:2")), LessThanTestCase.create(Version.fromString("3:2"), Version.fromString("25:2")), LessThanTestCase.create(Version.fromString("1:2:123"), Version.fromString("1:12:3")), LessThanTestCase.create(Version.fromString("1.2-3-5"), Version.fromString("1.2-5")), LessThanTestCase.create(Version.fromString("5.005"), Version.fromString("5.10.0")), LessThanTestCase.create(Version.fromString("3.10.2"), Version.fromString("3a9.8")), LessThanTestCase.create(Version.fromString("3~10"), Version.fromString("3a9.8")), LessThanTestCase.create( Version.fromString("1.4+OOo3.0.0~"), Version.fromString("1.4+OOo3.0.0-4")), LessThanTestCase.create(Version.fromString("3.0~rc1-1"), Version.fromString("3.0-1")), LessThanTestCase.create(Version.fromString("2.4.7-1"), Version.fromString("2.4.7-z")), LessThanTestCase.create(Version.fromString("1.00"), Version.fromString("1.002-1+b2")), LessThanTestCase.create(Version.fromString("5.36-r0"), Version.fromString("5.36")), LessThanTestCase.create(Version.fromString("5.36-r0"), Version.fromString("5.36-gg1.0")), LessThanTestCase.create( Version.fromString("5.36-r0"), Version.fromString("5.36-r0-gg1.0"))); } @Theory public void compareTo_lessThanTestCase_hasCorrectSymmetryResult( @FromDataPoints("LessThan") LessThanTestCase testCase) { assertThat(testCase.smaller()).isLessThan(testCase.larger()); assertThat(testCase.larger()).isGreaterThan(testCase.smaller()); } } ================================================ FILE: common/src/test/resources/com/google/tsunami/common/net/http/testdata/README.md ================================================ # HTTP Lib Testdata ## tsunami_test_server.p12 This is a PKCS12 self-signed server key/cert file. This file was generated using the following commands with the password `tsunamitest`: ```shell $ openssl req -new -x509 -nodes -sha1 -days 3650 \ -out /tmp/tsunami_test_server.crt \ -keyout /tmp/tsunami_test_server.key # Password is "tsunamitest" without the quotes. $ openssl pkcs12 -export -clcerts \ -in /tmp/tsunami_test_server.crt \ -inkey /tmp/tsunami_test_server.key \ -out tsunami_test_server.p12 ``` ================================================ FILE: core.Dockerfile ================================================ # Stage 1: Build phase FROM ghcr.io/google/tsunami-scanner-devel:latest AS build ## build the core engine WORKDIR /usr/repos/tsunami-security-scanner COPY . . RUN mkdir -p /usr/tsunami RUN gradle shadowJar RUN find . -name 'tsunami-main-*.jar' -exec cp {} /usr/tsunami/tsunami.jar \; RUN cp ./tsunami_tcs.yaml /usr/tsunami/tsunami.yaml RUN cp plugin/src/main/resources/com/google/tsunami/plugin/payload/payload_definitions.yaml /usr/tsunami/payload_definitions.yaml RUN cp -r plugin_server/py/ /usr/tsunami/py_server ## We perform a hotpatch of the path pointing to the payload definitions file ## for easier usage in the Dockerized environment. RUN sed -i "s%'../../plugin/src/main/resources/com/google/tsunami/plugin/payload/payload_definitions.yaml'%'/usr/tsunami/payload_definitions.yaml'%g" \ /usr/tsunami/py_server/plugin/payload/payload_utility.py ## generate the protos for Python plugins WORKDIR /usr/repos/tsunami-security-scanner/ RUN python3 -m grpc_tools.protoc \ -I/usr/repos/tsunami-security-scanner/proto \ --python_out=/usr/tsunami/py_server/ \ --grpc_python_out=/usr/tsunami/py_server/ \ /usr/repos/tsunami-security-scanner/proto/*.proto # Stage 2: Release FROM scratch AS release COPY --from=build /usr/tsunami/tsunami.jar /usr/tsunami/ COPY --from=build /usr/tsunami/tsunami.yaml /usr/tsunami/ COPY --from=build /usr/tsunami/payload_definitions.yaml /usr/tsunami/payload_definitions.yaml # Python server and the virtual environment COPY --from=build /usr/tsunami/py_venv/ /usr/tsunami/py_venv COPY --from=build /usr/tsunami/py_server/ /usr/tsunami/py_server ================================================ FILE: devel.Dockerfile ================================================ FROM ubuntu:latest RUN apt-get update \ && apt-get install -y --no-install-recommends git openjdk-21-jdk ca-certificates wget unzip python3 python3-venv \ && rm -rf /var/lib/apt/lists/* \ && rm -rf /usr/share/doc && rm -rf /usr/share/man \ && apt-get clean # Install a specific version of protoc for the templated plugins WORKDIR /usr/dependencies RUN mkdir /usr/dependencies/protoc \ && wget https://github.com/protocolbuffers/protobuf/releases/download/v25.5/protoc-25.5-linux-x86_64.zip -O /usr/dependencies/protoc.zip \ && unzip /usr/dependencies/protoc.zip -d /usr/dependencies/protoc/ ENV PATH="${PATH}:/usr/dependencies/protoc/bin" # Install a specific version of Gradle WORKDIR /usr/dependencies RUN wget https://services.gradle.org/distributions/gradle-8.14.2-bin.zip -O /usr/dependencies/gradle.zip \ && unzip /usr/dependencies/gradle.zip -d /usr/dependencies/ \ && mv /usr/dependencies/gradle-8.14.2/ /usr/dependencies/gradle/ ENV PATH="${PATH}:/usr/dependencies/gradle/bin" # Prepare the virtualenv for Python plugins # This is one of the few dependencies that will get carried to the final docker # images. WORKDIR /usr/tsunami/py_venv/ COPY plugin_server/py/requirements.in /usr/tsunami/py_venv/requirements.in COPY plugin_server/py/requirements.txt /usr/tsunami/py_venv/requirements.txt RUN python3 -m venv /usr/tsunami/py_venv ENV PATH="/usr/tsunami/py_venv/bin:${PATH}" RUN pip install --require-hashes -r /usr/tsunami/py_venv/requirements.txt ================================================ FILE: docs/_config.yml ================================================ remote_theme: pages-themes/cayman@v0.2.0 url: https://google.github.io baseurl: /tsunami-security-scanner paginate: 5 paginate_path: "/blog/page:num/" plugins: - jekyll-remote-theme - jekyll-paginate ================================================ FILE: docs/_data/nav.yml ================================================ - title: "What's new" path: / - title: "All articles" path: /blog/ - title: "Documentation" path: /howto/ ================================================ FILE: docs/_includes/nav.html ================================================ {% for nav in site.data.nav %} {% if nav.subcategories != null %} {% for subcategory in nav.subcategories %} {{ subcategory.title }} {% endfor %} {% elsif nav.title == page.title %} {{ nav.title }} {% else %} {{ nav.title }} {% endif %} {% endfor %} ================================================ FILE: docs/_layouts/default.html ================================================ {% seo %} {% include head-custom.html %} Skip to the content.
{{ content }}
================================================ FILE: docs/_layouts/home.html ================================================ --- layout: none --- {{ site.posts.first }} ================================================ FILE: docs/_layouts/post.html ================================================ --- layout: default ---

Posted on {{ page.date | date_to_long_string: "ordinal" }} by {% for author in page.authors %} {{ author.name }} {% endfor %}

{{ content }} ================================================ FILE: docs/_posts/2024-03-19-tsunami-network-scanner-ai-security.md ================================================ --- authors: - name: Annie Mao excerpt: 'Interested in creating an AI-related plugin for the Tsunami network scanner and getting rewarded for your efforts? See this post for details!' title: 'Tsunami Network Scanner & AI Security' --- You may already be familiar with the [Tsunami Network Scanner](https://github.com/google/tsunami-security-scanner) from our [Patch Rewards program](https://bughunters.google.com/about/rules/4928084514701312/patch-rewards-program-rules#tsunami-patch-rewards), which rewards external contributors for creating new [detector plugins](https://github.com/google/tsunami-security-scanner-plugins/tree/master/google). Now with AI being on everyone's minds, we want to double down on securing open source AI infrastructure via Tsunami. On our [GitHub page](https://github.com/google/tsunami-security-scanner-plugins/issues), you can find a list of AI-relevant **plugin & web fingerprint** implementation requests tagged as "help wanted". **Anyone** can contribute to a Tsunami plugin from this list, and the implementation will be reviewed & rewarded under our Tsunami Patch Rewards program, with rewards ranging from $500 to $3,133.7 ([details](https://bughunters.google.com/about/rules/4928084514701312/patch-rewards-program-rules#reward-amounts-tsunami-)). Here are the rules of engagement for implementing AI-related plugins: * **First come, first served**: Each contributor can pick up any of the unassigned plugins, but please only take one **at a time**. * **Reassignment of inactive plugins**: If an assigned plugin has not been worked on for **over a week**, then the Tsunami review panel will unassign the contributor from the plugin. The plugin request is returned to the free-for-all pool. * **Vulnerability Research**: As a first step, the contributor has to provide detailed vulnerability research & an implementation design for the plugin to the review panel, and then wait for confirmation from the review panel before moving on to the implementation stage. * **Testbed Requirement**: All test containers or configurations for each plugin have to be submitted to [google/security-testbeds](https://github.com/google/security-testbeds). * **Review Priority**: If a contributor already has a different plugin in the review queue, we will prioritize reviewing the ML plugin, unless the originally provided plugin is critical. Finally, we welcome you to propose new plugins that address critical security issues in AI-serving frameworks and related tools on our [GitHub page](https://github.com/google/tsunami-security-scanner-plugins/issues). For faster acceptance, when sharing your proposal, please provide context on how a given service is used in the AI ecosystem. We're looking forward to collaborating with you to keep AI infrastructure secure! ================================================ FILE: docs/_posts/2025-06-18-changes-to-tsunami.md ================================================ --- authors: - name: Pierre Precourt excerpt: ‘Templated plugins are now the default for writing plugins and making the reward program more efficient.' title: 'Changes to Tsunami' --- # Changes to Tsunami ## Improving the situation on the Patch Reward Program (PRP) Whether you have been a long time contributor or a newcomer to Tsunami, you might have noticed that it takes a long time before a contribution is merged. We thought it might be valuable to provide some context into why this happens: - First and foremost, the Tsunami team is a rather small team (1-2 people) with varying priorities. - Even though we are working with partners to reduce the time to review contributions, it still takes us a lot of time to merge contributions. That is because the external version of Tsunami and the one we are using internally are slightly different, most notably their build systems. Now, this does not mean that we should not strive to make the situation better. One bet that we are taking is templated plugins (introduced later in this article). Without going into details, this new type of plugin should abstract the differences between the two build systems, in a way that should make merging plugins much easier for us and, thus, much faster for contributors. ## Tsunami templated plugins To cite [our official documentation](https://google.github.io/tsunami-security-scanner/howto/new-detector/templated/00-getting-started): > In the past, if you wanted to write a Tsunami detector, you would need to > implement your detector using Java or Python. For each, you would have to > write a set of tests and ensure that everything is compiling and working as > intended. > This process proved to be very time consuming; especially as most Tsunami > detectors are simply sending an HTTP request and checking the response code > and body content. That is why we introduced templated plugins. > We have abstracted most of the code required to write a plugin. All you need > to do is to write a .textproto file that describes the behavior of your plugin > and a _test.textproto file that describes the tests for the plugin. In short, templated plugins allow contributors to write Tsunami plugins as if they were configuration files. ### Why this change? First and foremost, this makes writing Tsunami plugins much easier. It reduces adherence to the build system and to the language, which makes it accessible to more contributors. Because the plugins are now in a structured format, we can also perform analysis on detectors and find common mistakes in these detectors: this should overall improve the quality of plugins. But that is only from the contributors’ perspective. From a maintainer perspective, that also means that we can work more efficiently on changes at scale in the Tsunami engine. Currently, whenever we need to make a change to the engine, we often have to change about 100 plugins. Finally, we are having very active discussions internally to rewrite Tsunami entirely in Golang. If we were to decide to take this path, templated plugins would help us make the migration easier. ### Why not YAML? The most frequently asked question about templated plugins is: Why not use YAML instead of textproto? First, we believe that [YAML has a lot of issues](https://ruudvanasseldonk.com/2023/01/11/the-yaml-document-from-hell). But the most important reason is that textprotos are checked at compilation time and enforce a strong structure to our plugins (on top of being smaller). ### How does this affect the Patch Reward Program (PRP)? Templated plugins are now the default for writing plugins. **We will stop accepting Java and Python plugins unless there is a good reason for it (we understand that templated plugins can be limiting in some use cases)**. Other than that, no big changes. The rewards will stay the same and so will the queue system. If our bet shows to be successful and templated plugins really reduce the time for plugins to be merged, we plan to increase the contributor queue as well. This would mean that a contributor could work on more plugins in parallel. Stay tuned. ### Will older plugins be rewritten? Most likely. We have not yet come-up with a detailed plan on how to do it, but we would like to rewrite as many plugins as possible to unify them. ## Tsunami releases ## Tsunami version 1.0.0 For a long time, Tsunami has been in Alpha release. Internally, we have been using Tsunami consistently and at scale for a while now. Thus we believe that Tsunami is ready to be officially released in version 1.0.0. ### Maven releases For now, we are releasing most of Tsunami’s library to maven on repo central. If you are depending on these artifacts, you will soon have to migrate away. We are planning to change the way we are publishing Tsunami’s dependencies. The plan is not finalized yet, but most likely we will publish Tsunami directly on GitHub. ================================================ FILE: docs/_posts/2025-10-16-october-update-tsunami-prp.md ================================================ # October update - Tsunami reward program ## Improving the PRP situation Since our [last update in June](https://google.github.io/tsunami-security-scanner/2025/06/18/changes-to-tsunami.html), we have made good progress on merging incoming pull requests. Not only do we now have a very low amount of requests to process, but most of them are now implemented with the [new templated language system](https://google.github.io/tsunami-security-scanner/howto/new-detector/templated/00-getting-started) which is usually faster for us to merge. **A big thank you to all of our contributors for their patience\!** ## An update on the payouts Note: [Our official rules](https://bughunters.google.com/about/rules/open-source/5067456626688000/tsunami-patch-rewards-program-rules) have been updated accordingly. We recently came to realize that our current payout system made the decision for the reward difficult. To ensure everyone is rewarded fairly and adequately, we have decided to simplify the payout system: Type of detector | Reward (up to dollars) :--------------------------------------------------: | :--------------------: Wishlist detector | 3177.13 Exposed interface detector Weak credentials detector | 2000 Other detectors | 1500 ### What is a wishlist detector? This is a detector for a vulnerability that Google cares deeply about. We understand that this is outside of the control of the contributors but this is generally based on internal priorities. We will generally make it explicit that a contribution falls in that category but on the other hand, we might request that the detector is completed in a faster timeline (less than a week) to justify the higher payout. Sometimes we will release a wishlist to the public – if you pick up an item from that wishlist, you are guaranteed to fall into this category. ### What happened to fingerprints? We are not accepting new fingerprinting contributions for now. **Note that pull requests already opened will be processed and paid as previously agreed upon.** We are currently working on completely changing the way Tsunami performs fingerprinting. Amongst other things, we are experimenting with rewriting that specific portion of the scanner in Golang to measure how well the language matches our needs. ## An insight into our triage decisions We also understand that it might be difficult to understand how and why we decide to accept some contributions and not others, so we wanted to provide some visibility into that process. First and foremost, the goal of Tsunami is to find impactful vulnerabilities. **This generally means that we want to identify security issues that have a strong impact; this generally translates to remote code execution (RCE).** **The questions that we are always asking ourselves:** * Can this be turned into a full-chain to remote code execution? * Can the full-chain be implemented in the detector? Or be reliable enough that it can ascertain the full chain exploitability? Here is an example table for common vulnerability types: | Category | Decision | | :----------------------: | :-------------: | | XSS | Rejected | | CSRF | Rejected | | SSRF | Likely rejected | | SQLi | Likely rejected | | Local file include | It depends | | Path traversal | It depends | | XXE | It depends | | Remote file include | Likely accepted | | File upload | Likely accepted | | Exposed interface | Likely accepted | | Authentication bypass | Likely accepted | | Weak credentials | Likely accepted | | OS command injection | Likely accepted | As mentioned before, that decision depends heavily on the ability to create a full chain of exploitation that leads to remote code execution. ## Tsunami versioning As we previously announced, we are slowly dropping Maven releases in favor of our Docker images and direct dependencies to GitHub. We are already not publishing any new artifacts to Maven and encourage you **strongly** to migrate to building with the GitHub code. This change slightly increases overall maintenance of plugins for larger changes of the core but ensures that issues do not go unnoticed and also makes dependencies management a lot easier for us. ================================================ FILE: docs/about/index.md ================================================ # About Tsunami ## Why Tsunami? When security vulnerabilities or misconfigurations are actively exploited by attackers, organizations need to react quickly in order to protect potentially vulnerable assets. As attackers increasingly invest in automation, the time window to react to a newly released, high severity vulnerability is usually measured in hours. This poses a significant challenge for large organizations with thousands or even millions of internet-connected systems. In such hyperscale environments, security vulnerabilities must be detected and ideally remediated in a fully automated fashion. To do so, information security teams need to have the ability to implement and roll out detectors for novel security issues at scale in a very short amount of time. Furthermore, it is important that the detection quality is consistently very high. To solve these challenges, we created Tsunami - an extensible network scanning engine for detecting high severity vulnerabilities with high confidence in an unauthenticated manner. ## Goals and Philosophy * Tsunami supports small manually curated set of vulnerabilities * Tsunami detects high severity, RCE-like vulnerabilities, which often actively exploited in the wild * Tsunami generates scan results with high confidence and minimal false-positive rate. * Tsunami detectors are easy to implement. * Tsunami is easy to scale, executes fast and scans non-intrusively. ## Naming The name "Tsunami" comes from the fact that this scanner is meant be used as part of a larger system to warn owners about automated "attack waves". Automated attacks are similar to tsunamis in the way that they come suddenly, without prior warning and can cause a lot of damage to organizations if no precautions are taken. The term "Tsunami Early Warning System Security Scanning Engine" is quite long and thus the name got abbreviated to Tsunami Scanning Engine, or Tsunami. Hence, the name is not an analogy to tsunamis itself, but to a system that detects them and warns everyone about them. ================================================ FILE: docs/assets/css/style.scss ================================================ --- --- @import '{{ site.theme }}'; .pagination { text-align: center; background-color: #eee; border-radius: 0.3rem; padding: 3px; margin-top: 0.75rem; margin-bottom: 0.75rem; } ================================================ FILE: docs/blog/index.html ================================================ --- title: Posts layout: default --- {% for post in paginator.posts %}

{{ post.title }}

Posted on {{ post.date | date_to_long_string: "ordinal" }}

{{ post.excerpt }}
{% endfor %} ================================================ FILE: docs/contribute/code-of-conduct.md ================================================ # Google Open Source Community Guidelines At Google, we recognize and celebrate the creativity and collaboration of open source contributors and the diversity of skills, experiences, cultures, and opinions they bring to the projects and communities they participate in. Every one of Google's open source projects and communities are inclusive environments, based on treating all individuals respectfully, regardless of gender identity and expression, sexual orientation, disabilities, neurodiversity, physical appearance, body size, ethnicity, nationality, race, age, religion, or similar personal characteristic. We value diverse opinions, but we value respectful behavior more. Respectful behavior includes: * Being considerate, kind, constructive, and helpful. * Not engaging in demeaning, discriminatory, harassing, hateful, sexualized, or physically threatening behavior, speech, and imagery. * Not engaging in unwanted physical contact. Some Google open source projects [may adopt][] an explicit project code of conduct, which may have additional detailed expectations for participants. Most of those projects will use our [modified Contributor Covenant][]. [may adopt]: https://opensource.google/docs/releasing/preparing/#conduct [modified Contributor Covenant]: https://opensource.google/docs/releasing/template/CODE_OF_CONDUCT/ ## Resolve peacefully We do not believe that all conflict is necessarily bad; healthy debate and disagreement often yields positive results. However, it is never okay to be disrespectful. If you see someone behaving disrespectfully, you are encouraged to address the behavior directly with those involved. Many issues can be resolved quickly and easily, and this gives people more control over the outcome of their dispute. If you are unable to resolve the matter for any reason, or if the behavior is threatening or harassing, report it. We are dedicated to providing an environment where participants feel welcome and safe. ## Reporting problems Some Google open source projects may adopt a project-specific code of conduct. In those cases, a Google employee will be identified as the Project Steward, who will receive and handle reports of code of conduct violations. In the event that a project hasn’t identified a Project Steward, you can report problems by emailing opensource@google.com. We will investigate every complaint, but you may not receive a direct response. We will use our discretion in determining when and how to follow up on reported incidents, which may range from not taking action to permanent expulsion from the project and project-sponsored spaces. We will notify the accused of the report and provide them an opportunity to discuss it before any action is taken. The identity of the reporter will be omitted from the details of the report supplied to the accused. In potentially harmful situations, such as ongoing harassment or threats to anyone's safety, we may take action without notice. *This document was adapted from the [IndieWeb Code of Conduct][] and can also be found at .* [IndieWeb Code of Conduct]: https://indieweb.org/code-of-conduct ================================================ FILE: docs/contribute/contributing.md ================================================ # How to Contribute We'd love to accept your patches and contributions to this project. There are just a few small guidelines you need to follow. ## Contributor License Agreement Contributions to this project must be accompanied by a Contributor License Agreement. You (or your employer) retain the copyright to your contribution; this simply gives us permission to use and redistribute your contributions as part of the project. Head over to to see your current agreements on file or to sign a new one. You generally only need to submit a CLA once, so if you've already submitted one (even if it was for a different project), you probably don't need to do it again. ## Code reviews All submissions, including submissions by project members, require review. We use GitHub pull requests for this purpose. Consult [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more information on using pull requests. ## Community Guidelines This project follows [Google's Open Source Community Guidelines](https://opensource.google/conduct/). ================================================ FILE: docs/contribute/index.md ================================================ # Contributing to Tsunami {% include_relative contributing.md %} {% include_relative code-of-conduct.md %} ================================================ FILE: docs/howto/common-patterns.md ================================================ # Common detector patterns ### Running only for a specific service *Use case: I want my detector to only run for web applications or for application X.* There exist currently two way in Tsunami to filter the service type: 1. Using annotations (preferred) The [`@ForWebService`](https://github.com/google/tsunami-security-scanner/blob/master/plugin/src/main/java/com/google/tsunami/plugin/annotations/ForWebService.java) and [`@ForServiceName({"X", "Y"})`](https://github.com/google/tsunami-security-scanner/blob/master/plugin/src/main/java/com/google/tsunami/plugin/annotations/ForServiceName.java) annotations can be used to instruct the core engine of Tsunami to only run this plugin if the service was a web service or a service with name `X` or `Y`. The name of the service is obtained during the discovery phase. It currently is the exact same service name as NMAP would report (e.g. `http`, `https`, `ssh`). 2. Using filtering (web service only) The [`NetworkServiceUtils.isWebService()`](https://github.com/google/tsunami-security-scanner/blob/483f9ea5b7c69e8802353e0dcd293c2f35eaa4aa/common/src/main/java/com/google/tsunami/common/data/NetworkServiceUtils.java#L69) can be used when performing filtering to ensure only `NetworkService` that were identified as web service will be processed. Example usage: ```java someNetworkServiceCollection.stream() .filter(NetworkServiceUtils::isWebService) // {...} ``` ### Building URLs *Use case: My detector targets a web application. How do I build the URL?* When writing your plugins, there are a few things that you should NOT have to care about and that the Tsunami core engine should resolve for you: - Is the service using HTTP or HTTPS? - How do I construct the URL from the `NetworkService`? - What if NMAP fails to identify the service as HTTP and I still want to build an URL? All of these concerns are addressed in the core engine of Tsunami and you can simply use the URL building API: [`NetworkServiceUtils.buildWebApplicationRootUrl()`](https://github.com/google/tsunami-security-scanner/blob/483f9ea5b7c69e8802353e0dcd293c2f35eaa4aa/common/src/main/java/com/google/tsunami/common/data/NetworkServiceUtils.java#L173) #### DO ```java String myUrl = NetworkServiceUtils.buildWebApplicationRootUrl(networkService) + MY_VULNERABLE_ENDPOINT; ``` #### DO NOT The following **SHOULD NOT BE USED**: 1. Defining a `buildTarget` intermediate function is redundant and most of the time not necessary: ```java private static StringBuilder buildTarget(NetworkService networkService) { StringBuilder targetUrlBuilder = new StringBuilder(); if (NetworkServiceUtils.isWebService(networkService)) { targetUrlBuilder.append(NetworkServiceUtils.buildWebApplicationRootUrl(networkService)); } else { targetUrlBuilder .append("http://") .append(toUriAuthority(networkService.getNetworkEndpoint())) .append("/"); } targetUrlBuilder.append(MY_VULNERABLE_ENDPOINT); return targetUrlBuilder; } ``` 2. Using `String.Format` does not make use of the information obtained during the discovery phase and is error prone: ```java var uriAuthority = NetworkEndpointUtils.toUriAuthority(networkService.getNetworkEndpoint()); var loginPageUrl = String.format("http://%s/%s", uriAuthority, MY_VULNERABLE_ENDPOINT); ``` ### Adding command line arguments consumed by the detector *Use case: I need command line arguments for my detector* Tsunami uses [jCommander](https://jcommander.org/) for command line argument parsing. In order to add new CLI arguments for your plugin, first define the data class for holding all the arguments. You can follow the jCommander tutorial to learn more about this tool. For example: ```java @Parameters(separators = "=") public final class MyPluginArgs implements CliOption { @Parameter(names = "--param", description = "Description for param.") public String param; @Override public void validate() { // Validate the command line value. } } ``` Then, the CLI flags will be parsed and an instance of this class will be created by the main scanner at start-up time. In order to use this class in your plugin, you can directly inject this data class into your plugin's constructor. ```java public final class MyVulnDetector implements VulnDetector { private final MyPluginArgs args; @Inject MyVulnDetector(MyPluginArgs args) { this.args = checkNotNull(args); } // {...} } ``` ### Adding configuration properties consumed by the detector *Use case: How do I add configurable option for my detector?* Tsunami supports loading configs from a YAML file and uses [snakeyaml](https://bitbucket.org/asomov/snakeyaml/wiki/Documentation) to parse the YAML config files. In order to add configuration properties to your plugin, first you need to define a data class for holding all the configuration values. NOTE: Currently Tsunami only supports standard Java data types for configurations like `String`, numbers (`int`, `long`, `float`, `double`, etc), `List` and `Map`. ```java // All config classes must be annotated by this ConfigProperties annotation in // order for Tsunami to recognize the config class. @ConfigProperties(prefix = "my.plugin.configs") public class MyPluginConfigs { String stringValue; long longValue; List listValues; Map mapValues; } ``` Then, similar to the command line arguments, you can inject an instance of this data class into your plugin's constructor. ```java public final class MyVulnDetector implements VulnDetector { private final MyPluginConfigs configs; @Inject MyVulnDetector(MyPluginConfigs configs) { this.configs = checkNotNull(configs); } // {...} } ``` The scanner will parse the configuration file when it starts, create an instance of the data class from the config data, and inject the instance into your plugin. Following is an example config file for the previously defined `MyPluginConfigs` object. ```yaml my: plugin: configs: # Config name can be exact match. stringValue: "example value" # Or matching via snake_case. long_value: 123 list_values: - "a" - "b" - "c" mapValues: key1: "value1" key2: "value2" ``` To use the configuration file, you need to set the `tsunami.config.location` option when calling Tsunami. ### Creating TCP sockets with proper timeouts *Use case: My detector needs to create raw TCP or SSL sockets to communicate with a service.* When writing detectors that need to create raw TCP or SSL sockets, you **must** use the `TsunamiSocketFactory` API instead of directly creating sockets through `javax.net.SocketFactory` or `javax.net.ssl.SSLSocketFactory`. This ensures that all sockets have proper timeout configurations, preventing plugins from hanging indefinitely when servers don't respond. #### Injecting the socket factory ```java public final class MyVulnDetector implements VulnDetector { private final TsunamiSocketFactory socketFactory; @Inject MyVulnDetector(TsunamiSocketFactory socketFactory) { this.socketFactory = checkNotNull(socketFactory); } // {...} } ``` #### Creating a plain TCP socket with default timeouts ```java private final TsunamiSocketFactory socketFactory; // Socket will have connect timeout of 10s and read timeout of 30s (configurable) Socket socket = socketFactory.createSocket("example.com", 80); try { // Use the socket... OutputStream out = socket.getOutputStream(); InputStream in = socket.getInputStream(); // ... } finally { socket.close(); } ``` #### Creating a socket with custom timeouts ```java Socket socket = socketFactory.createSocket( "example.com", 80, Duration.ofSeconds(5), // Connect timeout Duration.ofSeconds(15) // Read timeout ); ``` #### Creating an SSL/TLS socket ```java // Create an SSL socket with default timeouts SSLSocket sslSocket = socketFactory.createSslSocket("secure.example.com", 443); // Or with custom timeouts SSLSocket sslSocket = socketFactory.createSslSocket( "secure.example.com", 443, Duration.ofSeconds(5), Duration.ofSeconds(15) ); ``` #### Upgrading a plain socket to SSL (STARTTLS) ```java // First, create a plain socket Socket plainSocket = socketFactory.createSocket("mail.example.com", 25); // Send STARTTLS command... // ... // Then upgrade to SSL SSLSocket sslSocket = socketFactory.wrapWithSsl( plainSocket, "mail.example.com", 25, true // autoClose ); ``` #### Configuring socket timeouts Socket timeouts can be configured via: 1. **Configuration file** (tsunami.yaml): ```yaml common: net: socket: connect_timeout_seconds: 10 read_timeout_seconds: 30 trust_all_certificates: true ``` 1. **Command line options**: ```bash --socket-connect-timeout-seconds=10 --socket-read-timeout-seconds=30 --socket-trust-all-certificates=true ``` CLI options take precedence over configuration file settings. #### DO NOT create sockets directly **NEVER** create sockets directly like this: ```java // BAD: No timeout configured, socket may hang forever Socket socket = new Socket("example.com", 80); // BAD: Even with SocketFactory, timeout is missing Socket socket = SocketFactory.getDefault().createSocket("example.com", 80); ``` Always use `TsunamiSocketFactory` to ensure proper timeout handling. ================================================ FILE: docs/howto/howto.md ================================================ # Build and run Tsunami ## Tsunami docker's environment We provide a set of Docker images to help you build and use Tsunami. We provide a minimal (scratch) image for: - The core engine only; - The callback server only; - Each category of plugin; Using these minimal images is not recommended, instead we recommend composing on top of them. ![docker-images](img/docker-images.png) ## Running the latest version of Tsunami If you just want to run the latest version of Tsunami, without having to recompile anything, you can directly use the latest full image of Tsunami. ```sh # Important: If you built a local version of the container, do not pull as it # will overwrite your changes. Otherwise, do pull as you would be using a stale # version of the image. $ docker pull ghcr.io/google/tsunami-scanner-full # Run the image $ docker run -it --rm ghcr.io/google/tsunami-scanner-full bash # If you want to use Python plugins (docker) $ tsunami-py-server >/tmp/py_server.log 2>&1 & # If you want to use the callback server (docker) $ tsunami-tcs >/tmp/tcs_server.log 2>&1 & # Run Tsunami # Note: If you did not start the python server, omit the `--python-` arguments. (docker) $ tsunami --ip-v4-target=127.0.0.1 --python-plugin-server-address=127.0.0.1 --python-plugin-server-port=34567 ``` This images contains everything necessary under the `/usr/tsunami` directory. To use the callback server, you might have to setup port forwarding with your docker container when starting it. We encourage you to refer to the `-p` option of Docker. A few tips: - Only scan one port: `--port-ranges-target` - Only run your detector: `--detectors-include="detector-name"`; where detector name is the name defined in `PluginInfo` section for Java and Python plugins and the `info.name` section on templated plugins. ## Using docker to build Tsunami In this section, we go through the different ways to compile the core engine or a plugin locally so that you can test your changes. It assumes that you have cloned both the `tsunami-security-scanner` and `tsunami-security-scanner-plugins` repositories. ### Rebuilding the core engine If you need to make changes to the core engine during the development cycle, you will have to perform the following actions to test your change: - Rebuild the core engine container; ```sh # Build the core engine container $ cd tsunami-security-scanner $ docker build -t ghcr.io/google/tsunami-scanner-core:latest -f core.Dockerfile . ``` - Rebuild all plugins to ensure your change is compatible IMPORTANT: Your changes must be committed via git to be picked. They do not need to be pushed to GitHub, they can be local only. In the following example, we will use docker volumes to mount our changes to `/usr/tsunami/repos/tsunami-security-scanner`. This assumes that our `tsunami-security-scanner` and `tsunami-security-scanner-plugins` clones are in `/tsunami/` on our host. Also, our changes are committed to the `master` branch. You can change the commands accordingly if your repositories path or branch are different. ```sh $ cd tsunami-security-scanner-plugins ``` First, we need to change the `Dockerfile` to use our changes: ```diff -ENV GITREPO_TSUNAMI_CORE="https://github.com/google/tsunami-security-scanner.git" -ENV GITBRANCH_TSUNAMI_CORE="stable" +ENV GITREPO_TSUNAMI_CORE="/usr/tsunami/repos/tsunami-security-scanner" +ENV GITBRANCH_TSUNAMI_CORE="master" ``` We also need to instruct docker to bind our changes in `/usr/tsunami/repos`: ```diff - RUN gradle build + RUN --mount=type=bind,source=/tsunami-security-scanner,target=/usr/tsunami/repos/tsunami-security-scanner \ + gradle build + ``` Then we can rebuild all plugins in one swoop: ```sh $ docker build -t ghcr.io/google/tsunami-plugins-all:latest --build-arg=TSUNAMI_PLUGIN_FOLDER=tsunami-security-scanner-plugins -f tsunami-security-scanner-plugins/Dockerfile /tsunami/ ``` - Rebuild the `-full` container; ```sh $ cd tsunami-security-scanner ``` We need to change the `full.Dockerfile` to use our newly created container: ```diff # Plugins - FROM ghcr.io/google/tsunami-plugins-google:latest AS plugins-google - FROM ghcr.io/google/tsunami-plugins-templated:latest AS plugins-templated - FROM ghcr.io/google/tsunami-plugins-doyensec:latest AS plugins-doyensec - FROM ghcr.io/google/tsunami-plugins-community:latest AS plugins-community - FROM ghcr.io/google/tsunami-plugins-govtech:latest AS plugins-govtech - FROM ghcr.io/google/tsunami-plugins-facebook:latest AS plugins-facebook - FROM ghcr.io/google/tsunami-plugins-python:latest AS plugins-python + FROM ghcr.io/google/tsunami-plugins-all:latest AS plugins-all {...} - COPY --from=plugins-google /usr/tsunami/plugins/ /usr/tsunami/plugins/ - COPY --from=plugins-templated /usr/tsunami/plugins/ /usr/tsunami/plugins/ - COPY --from=plugins-doyensec /usr/tsunami/plugins/ /usr/tsunami/plugins/ - COPY --from=plugins-community /usr/tsunami/plugins/ /usr/tsunami/plugins/ - COPY --from=plugins-govtech /usr/tsunami/plugins/ /usr/tsunami/plugins/ - COPY --from=plugins-facebook /usr/tsunami/plugins/ /usr/tsunami/plugins/ - COPY --from=plugins-python /usr/tsunami/py_plugins/ /usr/tsunami/py_plugins/ + COPY --from=plugins-all /usr/tsunami/plugins/ /usr/tsunami/plugins/ ``` And then rebuild it: ```sh $ docker build -t ghcr.io/google/tsunami-scanner-full:latest -f full.Dockerfile . ``` - Run the scanner to check that everything works. See the "Running the latest version of Tsunami" section on this page to run Tsunami with the newly built image. DO NOT perform a docker pull. ### Rebuilding a whole category of plugins Tsunami groups plugins per categories. From the root folder of the plugin repository, you can see that the categories are `google`, `community`, `templated` and so on. Our docker images are built separately for each category. The same Dockerfile is used, but it is parameterized to use a different folder with `TSUNAMI_PLUGIN_FOLDER`. ```sh $ cd tsunami-security-scanner-plugins $ build -t ghcr.io/google/tsunami-plugins-category:latest --build-arg TSUNAMI_PLUGIN_FOLDER=category . # For example with the community category: $ build -t ghcr.io/google/tsunami-plugins-community:latest --build-arg TSUNAMI_PLUGIN_FOLDER=community . ``` For **Python plugins**, you need to use the dedicated Dockerfile, which only supports bundling all plugins: ```sh $ cd tsunami-security-scanner-plugins $ build -t ghcr.io/google/tsunami-plugins-python:latest -f python.Dockerfile . ``` Once you have rebuilt the categories that you need, you can rebuild the `-full` image: ```sh $ cd tsunami-security-scanner $ docker build -t ghcr.io/google/tsunami-scanner-full:latest -f full.Dockerfile . ``` Then follow "Running the latest version of Tsunami" to use this new image. DO NOT perform a `docker pull`. ### Building an image for one plugin Now, if during development you only wish to build your plugin, you can do so by creating a new local-only category. Before you start, you will need to change the definition of the `full.Dockerfile` file: - Add a `FROM` directive in the Plugins section: ```diff FROM ghcr.io/google/tsunami-plugins-python:latest AS plugins-python + FROM ghcr.io/google/tsunami-plugins-local:latest AS plugins-local ``` - Add a `COPY` directive in the section that copies everything: ```diff COPY --from=plugins-python /usr/tsunami/py_plugins/ /usr/tsunami/py_plugins/ + COPY --from=plugins-local /usr/tsunami/plugins/ /usr/tsunami/plugins/ ``` Then, you can build the actual image containing only your plugin: ```sh $ cd tsunami-security-scanner-plugins $ build -t ghcr.io/google/tsunami-plugins-local:latest --build-arg TSUNAMI_PLUGIN_FOLDER=path/to/my/plugin . ``` Finally, compile the `-full` image: ```sh $ cd tsunami-security-scanner $ docker build -t ghcr.io/google/tsunami-scanner-full:latest -f full.Dockerfile . ``` Then follow "Running the latest version of Tsunami" to use this new image. DO NOT perform a `docker pull`. **Python plugins** do not support building only one plugin. See building the whole category instead. ## Building Tsunami without docker We do not provide support for building Tsunami outside of our docker environment. You can use the Dockerfile provided in the repositories to build your own toolchain if you so wish. ================================================ FILE: docs/howto/index.md ================================================ # Tsunami documentation Welcome to the Tsunami community, we are thrilled that you want to contribute. This page contains information to get you started with your first contributions. ## Contributing to Google code - [Contributing rules]({{ site.baseurl }}/contribute/index) ## Understanding Tsunami - [About Tsunami]({{ site.baseurl }}/about/index) - [How tsunami works]({{ site.baseurl }}/howto/orchestration) ## Building and running Tsunami - [How to build and run Tsumami]({{ site.baseurl }}/howto/howto) ## Writing plugins ### Vulnerability detectors - [Using our custom configuration format]({{ site.baseurl }}/howto/new-detector/templated/00-getting-started) (preferred approach) - [Using Java]({{ site.baseurl }}/howto/new-detector/new-detector-java) - Using Python *(not documented yet)* ### Weak credentials detectors Not documented yet. ### Fingerprinting plugins Not documented yet. ## Other guides - [Common detector patterns for Java plugins]({{ site.baseurl }}/howto/common-patterns) (i.e. "How do I do that?!") ================================================ FILE: docs/howto/new-detector/new-detector-java.md ================================================ # Writing a Tsunami detector (Java) NOTE: We now expect you to write plugins using the templated format first. Only resort to Java or Python if the plugin cannot be written with the templated format. ## Overview Each Tsunami detector needs the following pieces which we will create in this tutorial: * A plugin name that is unique among all enabled Tsunami plugins. * A set of build rules for [Gradle](https://gradle.org/) (external build) * A `VulnDetector` that implements the vulnerability detection logic. * A `PluginBootstrapModule` that provides necessary Guice bindings for the detector. * An optional `CliOption` that captures all the supported command line flags for the detector. * An optional `ConfigProperties` that captures all the supported configuration for the detector. ## 1. Fork the examples Tsunami provides a few example implementations of a `VulnDetector` plugin. The examples live in the [examples directory](https://github.com/google/tsunami-security-scanner-plugins/tree/master/examples) * Update Java package names. The example `VulnDetector` plugin is defined under `com.google.tsunami.plugins.example` package. Refactor the package and class name according to your detector implementation. * Give a meaningful description to the Gradle build rule at `build.gradle`. We will work on the Gradle build rule later. * Rewrite the `README.md` file to have a good explanation of your `VulnDetector` plugin. ## 2. Putting the detector together ### 2.a - `PluginInfo` annotation All Tsunami plugins must be annotated by the `PluginInfo` annotation. Otherwise it cannot be identified by Tsunami scanner at runtime. This annotation provides the general information about the plugin to the core scanner. Following is an example usage of the `PluginInfo` annotation from our `WordPressInstallPageDetector`. ```java @PluginInfo( // VULN_DETECTION PluginType is required for a VulnDetector plugin. type = PluginType.VULN_DETECTION, // This gives a human readable name for your VulnDetector. Can be different // from your class name. name = "WordPressInstallPageDetector", // The current version of your plugin. version = "0.1", // A detailed description about what this plugin does. description = "This detector checks whether a WordPress install is unfinished. An unfinished WordPress" + " installation exposes the /wp-admin/install.php page, which allows attacker to set" + " the admin password and possibly compromise the system.", // The author of this plugin. author = "Tsunami Team (tsunami-dev@google.com)", // The guice module that bootstraps this plugin. bootstrapModule = WordPressInstallPageDetectorBootstrapModule.class) ``` ### 2.b - Define the `VulnDetector` plugin Each vulnerability detector plugin is an implementation of the `VulnDetector` interface. For this step we only explain the placeholder code, later you'll need to provide implementations for the class itself. Following is an example placeholder code from the `WordPressInstallPageDetector`: ```java // ... // annotations // ... public final class WordPressInstallPageDetector implements VulnDetector { // See https://github.com/google/flogger for details. private static final GoogleLogger logger = GoogleLogger.forEnclosingClass(); // Tsunami uses Guice (https://github.com/google/guice) to manage the // dependencies. @Inject WordPressInstallPageDetector( // Tsunami provides a UtcClock for production and FakeUtcClock for // testing purposes. @UtcClock Clock utcClock, // You can also inject other useful dependencies to your plugin code, e.g. // inject HttpClient if you need to interact with a web server. HttpClient httpClient) { } // The entrypoint of the VulnDetector. We will explain this later. @Override public DetectionReportList detect( TargetInfo targetInfo, ImmutableList matchedServices) { // implement me. } } ``` ### 2.c - Implement the main detection logic The main logic of the detection is expected to happen in the `detect` method, which expects two arguments: 1. [The `TargetInfo` proto](https://github.com/google/tsunami-security-scanner/blob/master/proto/reconnaissance.proto). This proto contains information that were gathered during the fingerprinting and discovery phase. 1. [The `NetworkService` list](https://github.com/google/tsunami-security-scanner/blob/master/proto/network_service.proto). This list contains all the identified network services that match the service filtering annotations. The main detection logic usually iterates over all the elements of the `matchedServices` parameter and checks whether the `NetworkService` is vulnerable to the vulnerability your plugin checks for. If any service is vulnerable, you'll need to build a `DetectionReport` proto that explains the identified vulnerability. Following is an example implementation from our `WordPressInstallPageDetector`: ```java @Override public DetectionReportList detect( TargetInfo targetInfo, ImmutableList matchedServices) { return DetectionReportList.newBuilder() .addAllDetectionReports( matchedServices.stream() // The WordPressInstallPageDetector only works for web services. .filter(NetworkServiceUtils::isWebService) // Detection logic that checks whether a web service exposes // a wordpress installation page. Omitted here for simplicity. .filter(this::isServiceVulnerable) // Build a DetectionReport when the web service is vulnerable. .map(networkService -> buildDetectionReport(targetInfo, networkService)) .collect(toImmutableList())) .build(); } private DetectionReport buildDetectionReport( TargetInfo scannedTarget, NetworkService vulnerableNetworkService) { return DetectionReport.newBuilder() .setTargetInfo(scannedTarget) .setNetworkService(vulnerableNetworkService) .setDetectionTimestamp(Timestamps.fromMillis(Instant.now(utcClock).toEpochMilli())) .setDetectionStatus(DetectionStatus.VULNERABILITY_VERIFIED) .setVulnerability( Vulnerability.newBuilder() .setMainId( VulnerabilityId.newBuilder() .setPublisher("GOOGLE") .setValue("UNFINISHED_WORD_PRESS_INSTALLATION")) .setSeverity(Severity.CRITICAL) .setTitle("Unfinished WordPress Installation") .setDescription( "An unfinished WordPress installation exposes the /wp-admin/install.php page," + " which allows attacker to set the admin password and possibly" + " compromise the system.")) .build(); } ``` ## 3. Preparing the `PluginBootstrapModule` Each Tsunami plugin must have a companion `PluginBootstrapModule` that provides the required Guice bindings and registers the plugin to the core engine. Creating a `PluginBootstrampModule` is rather simple if you only need to register the plugin. You only need to call `registerPlugin` within the `configurePlugin` method (e.g. [`ExampleVulnDetectorBootstrapModule`](https://github.com/google/tsunami-security-scanner-plugins/tree/master/examples/example_vuln_detector/src/main/java/com/google/tsunami/plugins/example/ExampleVulnDetectorBootstrapModule.java)). A more complete example is the [GenericWeakCredentialDetectorBootstrapModule](https://github.com/google/tsunami-security-scanner-plugins/blob/master/google/detectors/credentials/generic_weak_credential_detector/src/main/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/GenericWeakCredentialDetectorBootstrapModule.java) ================================================ FILE: docs/howto/new-detector/templated/00-getting-started.md ================================================ # Getting started with templated plugins In this documentation, you will learn how to write a plugin for Tsunami in the templated format. ## What will be covered - [Introduction](01-introduction) * What is a templated plugin? * How does it work? * How do I know how to write a templated plugin? * Execution workflow of a templated plugin - [Bootstrapping the plugin](02-bootstrapping) * Understanding the vulnerability * Providing information about our plugin and the vulnerability * Configuring the plugin - [Writing the first actions](03-first-actions) - [Putting it together in workflows](04-workflows) - [Using variables](05-variables) * Using variables * Extracting information to local variables * Predefined variables - [Using the callback server](06-callback-server) - [Using cleanup actions](07-cleanup-actions) - [Writing unit tests](08-writing-unit-tests) - Glossaries * [Predefined variables](glossary-predefined-variables) * [Magic tests URIs when mocking HTTP](glossary-tests-magic-uri) - Appendixes * [Convention: How to name a plugin](appendix-naming-plugin) * [Convention: How to name an action](appendix-naming-actions) * [Convention: Naming tests](appendix-naming-tests) * [Using the linter](appendix-using-linter) ## Let's get started! [Introduction](01-introduction) ================================================ FILE: docs/howto/new-detector/templated/01-introduction.md ================================================ # Introduction ## What is a templated plugin? In the past, if you wanted to write a Tsunami detector, you would need to implement your detector using Java or Python. For each, you would have to write a set of tests and ensure that everything is compiling and working as intended. This process proved to be very time consuming; especially as most Tsunami detectors are simply sending an HTTP request and checking the response code and body content. That is why we introduced templated plugins. ## How does it work? We have abstracted most of the code required to write a plugin. All you need to do is to write a `.textproto` file that describes the behavior of your plugin and a `_test.textproto` file that describes the tests for the plugin. A `.textproto` is a human-readable text representation of a protocol buffer message. If you are not familiar with protocol buffers, we recommend checking the [official documentation](https://protobuf.dev/), but for our use case, you can think of it as a strongly typed JSON or YAML. The `.textproto` files are compiled into binary format and embedded as resources to a meta plugin that we will refer to as the templated Tsunami plugin. At runtime, the templated plugin will interpret the behavior described in each file and dynamically create a new detector for it. ![High-level-overview](img/templated-how-it-works.png) ## How do I know how to write a templated plugin? We have tried to cover as much as possible in this documentation. But the configuration language is bound to evolve with time. If you have any doubt, the source of truth will always be the [proto definition](https://github.com/google/tsunami-security-scanner-plugins/tree/master/templated/templateddetector/proto) which we aim at keeping as straightforward and commented as possible. ## Execution workflow of a templated plugin Each plugin is defined by a set of two high-level concepts: **Actions** and **workflows**. - **Actions** are the basic unit of execution in a templated plugin. They are responsible for performing one specific... well... action and returning a boolean value that defines whether it was successful. An example action, in plain English, could be: > Send an HTTP request to the target and verify that the returned status code > is 200 - Once you have defined a set of actions, you need to be able to define an order in which they will be executed: this is the role of **workflows**. To write a plugin, we need to define a set of actions and put them in a specific order with workflows. But how are things executed? The engine processes plugins in the following way: 1. Extracts all the workflows and actions defined in the plugin; 2. Performs very basic checks to ensure that everything is well defined; 3. Goes through all the defined workflows and execute the first that matches the conditions it was defined with; 4. Executes every action of the workflow in order until one fails or all actions are successful; 5. If any action failed, there is no vulnerability. If all actions were successful, the vulnerability is present. Steps 4 and 5 are repeated for every network service found during the port scanning phase of Tsunami. We call steps 4 and 5 a **run of the workflow**. ## What is next [Bootstrapping the plugin](02-bootstrapping) ================================================ FILE: docs/howto/new-detector/templated/02-bootstrapping.md ================================================ # Bootstrapping the plugin *This section assumes that you already know how to [build and run Tsunami](https://google.github.io/tsunami-security-scanner/howto/howto).* ## Before starting Before we start, we encourage you to take a quick look at the setup guide for the linter of the configuration language. Your plugin will be expected to pass all the checks in this linter. Warnings will have to be justified before being merged as well. [See the instructions for the linter](appendix-using-linter) ## Our vulnerable application For this tutorial series, we will write a simple detector for a non-existing vulnerability `CVE-0123-12345`. We want our detector to: - *Fingerprint the application*: we only want to send other requests if we are sure that we are dealing with the potentially vulnerable application; So we are going to send an HTTP request to `/version` and check for the presence of the banner `MyVulnerableApp` in the `Server` header; - *Exploit the application*: once we are sure that we are dealing with the right application, we are going to send a `POST` request to `/exploit` with a custom payload `%{ print("tsunami_%d_marker", 1250*1+3) }%` in the `process` field. Then we are going to verify that it gets executed by verifying that the answer contains `tsunami_1253_marker`; *Note: In this example, we simulate a template injection vulnerability that is going to cause `%{ print("tsunami_%d_marker", 1250*1+3) }%` to be interpreted. The vulnerable language or the syntax have no importance. The result of `1250*1+3` is going to be replaced at the `%d` placeholder, resulting in `tsunami_1253_marker` being printed in the response.* ## Providing information about our plugin Let's create a new file in our local copy of the [Tsunami plugin repository](https://github.com/google/tsunami-security-scanner-plugins) as `templated/templateddetector/plugins/cve/0123/MyVulnerableApp_CVE_0123_12345.textproto`. Let's add the header that will help linters to understand which proto definition we are using: ```proto # proto-file: third_party/tsunami_plugins/templated/templateddetector/proto/templated_plugin.proto # proto-message: TemplatedPlugin ``` Then we provide basic information about our plugin. Note that the name of the plugin is used to **uniquely identify it and thus should be unique across all plugins**. ```proto info: { type: VULN_DETECTION name: "MyVulnerableApp_CVE_0123_12345" author: "Some developper " version: "1.0" } ``` Now is a good time to read about our [naming conventions for plugins](appendix-naming-plugin). ## Information about the vulnerability Let's add information about the vulnerability itself. These are the information that will be used to generate the vulnerability report and notify about the vulnerability. Important: whenever a CVE is associated with the vulnerability the `related_id` field must be filled with the associated CVE reference. ```proto finding: { main_id: { publisher: "GOOGLE" value: "SOME_PLUGIN_DETECTION" } title: "Example templated plugin" description: "This is an example templated plugin." recommendation: "No recommendation, this is an example." related_id: { publisher: "CVE" value: "CVE-0123-12345" } } ``` ## Configuring the plugin The next section in our file, is the `config` section. It allows some basic tuning of the plugin. For now, let's keep it empty: ```proto config: {} ``` But it is important to keep in mind that this section allows: - To enable `debug` mode. In debug mode, the plugin will generate highly verbose debugging messages. For example, it will log every HTTP request and response; - To switch the plugin to be `disabled`. Note that even when disabled (unless explicitly specified in the test file) the tests for that plugin will still be executed. If we were to run our plugin now, we would see the plugin being registered but we would get a non-blocking error at runtime because we have no workflow or action defined: ```sh { ... } Dec 31, 2024 11:07:58 AM com.google.tsunami.plugin.PluginBootstrapModule registerDynamicPlugin INFO: Dynamic plugin registered: MyVulnerableApp_CVE_0123_12345 { ... } Dec 31, 2024 11:08:15 AM com.google.tsunami.plugins.detectors.templateddetector.TemplatedDetector detect INFO: Starting detector: MyVulnerableApp_CVE_0123_12345 Dec 31, 2024 11:08:15 AM com.google.tsunami.plugins.detectors.templateddetector.TemplatedDetector detect SEVERE: No workflow matched the current setup. Is plugin 'MyVulnerableApp_CVE_0123_12345' misconfigured? { ... } ``` ## What is next [Writing the first actions](03-first-actions) ================================================ FILE: docs/howto/new-detector/templated/03-first-actions.md ================================================ # Writing the first actions Each action is defined by a name and a subtype. A subtype defines what the action is able to do: For example "HTTP request" is a subtype that allows to send an HTTP request and inspect responses. Let's start building our fingerprinting step with a very simple action: ```proto actions: { name: "fingerprinting" http_request: { method: GET uri: "/version" } } ``` In that action, named `fingerprinting` we simply send a `GET` HTTP request to `/version` on the currently targeted service. *Note that Tsunami will only send HTTP requests to services that have been identified as HTTP services during the port scanning phase.* But that action will always succeed because it does not verify anything in the response. Let's use the `response` field and add a condition on the status code: ```proto actions: { name: "fingerprinting" http_request: { method: GET uri: "/version" response: { http_status: 200 } } } ``` Now we want to ensure the header contains `MyVulnerableApp`: we need to use **expectations**. We can either use `expect_all` or `expect_any`, which perform checks on the response that must respectively "all be true" or "at least one true". Here, we have only one expectation, so we will use `expect_all` with one `conditions`: ```proto actions: { name: "fingerprinting" http_request: { method: GET uri: "/version" response: { http_status: 200 expect_all: { conditions: [ { header: { name: "Server" } contains: "MyVulnerableApp" } ] } } } } ``` The condition is that the `header` with `name` `Server` `contains` the string `MyVulnerableApp`. Now, let's build a `POST` request for our exploitation step: ```proto actions: { name: "exploitation" http_request: { method: POST uri: "/exploit" data: "process=%{ print(\"tsunami_%d_marker\", 1250*1+3) }%" response: { http_status: 200 expect_all: { conditions: [ { body: {} contains: "tsunami_1253_marker" } ] } } } } ``` This action is very similar to the previous one but: - It sends a `POST` request instead of a `GET` one; - As part of the `POST` it sends `process=%{ print(\"tsunami_%d_marker\", 1250*1+3) }%` as data; - The expectation has been changed to check that the response body contains `tsunami_1253_marker`. ## Redirects Note that by default, the HTTP client will follow any HTTP redirects. If you wish to change that behavior, you can configure your HTTP request using the `client_options` stanza. For example, with our previous request: ```proto actions: { name: "exploitation" http_request: { method: POST uri: "/exploit" data: "process=%{ print(\"tsunami_%d_marker\", 1250*1+3) }%" client_options: { disable_follow_redirects: true } response: { http_status: 200 expect_all: { conditions: [ { body: {} contains: "tsunami_1253_marker" } ] } } } } ``` ## What is next [Putting it together in workflows](04-workflows) ================================================ FILE: docs/howto/new-detector/templated/04-workflows.md ================================================ # Assembling the workflow A workflow is simply a list of the actions to be executed, in order: ```proto workflows: { actions: [ "fingerprinting", "exploitation" ] } ``` And that's it! We have written our first templated plugin. What happens if we run it against a default HTTP server? ```sh { ... } Dec 31, 2024 11:29:58 AM com.google.tsunami.plugin.PluginBootstrapModule registerDynamicPlugin INFO: Dynamic plugin registered: MyVulnerableApp_CVE_0123_12345 { ... } Dec 31, 2024 11:30:15 AM com.google.tsunami.plugins.detectors.templateddetector.TemplatedDetector detect INFO: Starting detector: MyVulnerableApp_CVE_0123_12345 Dec 31, 2024 11:30:15 AM com.google.tsunami.common.net.http.OkHttpHttpClient send INFO: Sending HTTP 'GET' request to 'http://127.0.0.1:9090/version'. Dec 31, 2024 11:30:15 AM com.google.tsunami.common.net.http.OkHttpHttpClient parseResponse INFO: Received HTTP response with code '404' for request to 'http://127.0.0.1:9090/version'. Dec 31, 2024 11:30:15 AM com.google.tsunami.plugins.detectors.templateddetector.TemplatedDetector runWorkflowForService INFO: No vulnerability found because action 'Fingerprint' failed. Dec 31, 2024 11:30:15 AM com.google.tsunami.plugin.PluginExecutorImpl buildSucceededResult INFO: MyVulnerableApp_CVE_0123_12345 plugin execution finished in 119 (ms) { ... } ``` As expected, a default HTTP server does not have a `Server` header that contains `MyVulnerableApp`. ## What is next [Using variables](05-variables) ================================================ FILE: docs/howto/new-detector/templated/05-variables.md ================================================ # Using variables In our action, we have hardcoded our payload, which is not super convenient and readable: ```proto actions: { name: "exploitation" http_request: { method: POST uri: "/exploit" data: "process=%{ print(\"tsunami_%d_marker\", 1250*1+3) }%" response: { http_status: 200 expect_all: { conditions: [ { body: {} contains: "tsunami_1253_marker" } ] } } } } ``` Let's try to move our payload into a variable. For this, we will define variables in our workflow: ```proto workflows: { variables: [ { name: "payload" value: "%{ print(\"tsunami_%d_marker\", 1250*1+3) }%" }, { name: "payload_result" value: "tsunami_1253_marker" } ] actions: [ "fingerprinting", "exploitation" ] } ``` Which can then be used in the action: {% raw %} ```proto actions: { name: "exploitation" http_request: { method: POST uri: "/exploit" data: "process={{ payload }}" response: { http_status: 200 expect_all: { conditions: [ { body: {} contains: "{{ payload_result }}" } ] } } } } ``` {% endraw %} IMPORTANT: The syntax for variables is requires **exactly** one space before and after the variable name, between the brackets. Otherwise, substitution will not happen. ## Extracting information to local variables Expectations are sufficient if we need to check that the response has a very specific content. But what if we need to extract some information from the response for later use? For example, what if we have an action that creates a job and we need the job name in a follow-up action to trigger the vulnerability? In that case we can use **extractions** and **local variables**. But what are local variables? There are two types of variables: - **Workflow variables**: Defined at the workflow level they mostly define static content. They will exist for the whole lifecycle of the workflow and will be reset to their defined value between workflow runs. Note that workflow variables are technically mutable, but we strongly recommend not mutating them (i.e. not using them in extractions). We used this type of variable in the previous example. - **Local variables**: These variables are more dynamic. They are defined from **extractions** and are only valid for the current workflow run. They are mostly used to extract information that will be used in a later action. For our previous example, let us assume that the `/version` page returns a CSRF token that we will need to use in the `/exploit` request. We can use a local variable to store that value: ```proto actions: { name: "fingerprinting" http_request: { method: GET uri: "/version" response: { http_status: 200 expect_all: { conditions: [ { header: { name: "Server" } contains: "MyVulnerableApp" } ] } extract_all: { patterns: [ { from_body: {} regexp: "CSRFToken=([a-zA-Z0-9_]+)" variable_name: "csrf_token" }, ] } } } } ``` In that example, we: - still verify that the server has the `Server` header that we expect; - also try to `extract_all` `patterns` `from_body` given regular expression `regexp` and store the result in the variable `variable_name` (here "csrf_token"). The extraction system offers **exactly** one capture group and extracting several information should be set into different patterns. WARNING: The use of extractions with `extract_any` should be done carefully. We very strongly recommend to only use `extract_any` with the **same variable name** between extractions. Doing otherwise makes the plugin potentially flaky and difficult to debug. ## Predefined variables Now would be a good time to look at the [list of variables](glossary-predefined-variables) that Tsunami provides. ## What is next [Using the callback server](06-callback-server) ================================================ FILE: docs/howto/new-detector/templated/06-callback-server.md ================================================ # Using the callback server If you wrote plugins for Tsunami before, you know about the callback server. If you have not, the callback server is a mechanism that allows us to check for vulnerabilities via an out-of-band mechanism. You can read more about it on the [dedicated GitHub repository](https://github.com/google/tsunami-security-scanner-callback-server). In a nutshell, the callback server works this way: 1. A secret is generated (and stored in `T_CBS_SECRET`); 2. This secret is hashed; 3. The exploit uses the hashed secret and the URL of the callback server (`T_CBS_URI`) to trigger an out-of-band communication with the callback server; 4. The secret can be used to ask the callback server if a communication using the hashed secret has been logged (i.e. if the vulnerability has been triggered); Currently, the templated plugin system does not support payload generation, so communication with the callback server has to be handled semi-manually. Every workflow run will automatically generate a new secret, its hash and the adequate trigger URL. That means that, as part of your exploit, you will need to make sure a request to the callback server is made with the trigger URL. The trigger URL is stored in the `T_CBS_URI` variable. For example, in our previous example we could change the payload to: {% raw %} ```proto { name: "payload" value: "%{ import os; os.system('curl {{ T_CBS_URI }}') }%" } ``` {% endraw %} `T_CBS_URI` would be replaced with the trigger URL and the callback server would receive a request on that endpoint if the vulnerability is triggered. Checking if the vulnerability was triggered then simply requires defining a new action: ```proto actions: { name: "check_callback_server_logs" callback_server: { action_type: CHECK } } ``` And, voila! But wait... How do we know, in our plugin, if the callback server is currently running? Do we not want to support both use cases? That is one of the features offered by workflows: `condition` allows you to define a condition for the workflow to be executed. With our example: {% raw %} ```proto # Workflow that uses the callback server. workflows: { condition: REQUIRES_CALLBACK_SERVER variables: [ { name: "payload" value: "%{ import os; os.system('curl {{ T_CBS_URI }}') }%" } ## note: empty string is always present in the body. This cancels out the ## body content expectation. { name: "payload_result" value: "" } ] actions: [ "fingerprinting", "exploitation", "check_callback_server_logs" ] } # Workflow that does not use the callback server. workflows: { variables: [ { name: "payload" value: "%{ print(\"tsunami_%d_marker\", 1250*1+3) }%" }, { name: "payload_result" value: "tsunami_1253_marker" } ] actions: [ "fingerprinting", "exploitation" ] } ``` {% endraw %} Note: Because workflow are interpreted in order, the one that is more restrictive needs to be defined first. Otherwise, the less restrictive workflow would always be the one running. ## What is next [Using cleanup actions](07-cleanup-actions) ================================================ FILE: docs/howto/new-detector/templated/07-cleanup-actions.md ================================================ # Cleanup actions In our example, none of the defined actions will modify the target. But what if we had some actions that we want to clean-up after? Cleanup actions are the solution. Let us assume an arbitrary example that would cause a change on the target: ```proto actions: { name: "create_docker_container" http_request: { method: POST uri: "/api/v1/create" data: "name=MySuperContainer" response: { http_status: 200 } } } ``` In that example, if the request is successful, a new container will be created on the target, but we want to make sure to delete that container afterwards. Because cleanup actions are normal actions, we can start by writing the deletion request: ```proto actions: { name: "cleanup_container" http_request: { method: POST uri: "/api/v1/delete" data: "name=MySuperContainer" response: { http_status: 200 } } } ``` This action will ensure the container is deleted. But now, how do we make sure it is executed after and only if the container has been created? We register it as a cleanup of the initial action: ```proto actions: { name: "create_docker_container" http_request: { method: POST uri: "/api/v1/create" data: "name=MySuperContainer" response: { http_status: 200 } } cleanup_actions: "cleanup_container" } ``` Note the newly added `cleanup_actions` entry. This will ensure the following: - If `create_docker_container` is not executed or fail, the cleanup action will not be executed; - If the `create_docker_container` is executed successfully, the cleanup action will always be executed at the end of the current workflow run; ## What is next [Writing unit tests](08-writing-unit-tests) ================================================ FILE: docs/howto/new-detector/templated/08-writing-unit-tests.md ================================================ # Writing unit tests For every workflow, we expect to see associated unit tests for each of your contributions. If unit tests are not as reliable as integration testing, especially in the context of the scanner, they provide some insurance that changes are not breaking detectors. ## Configuration of the unit test The tests for a specific plugin are defined in a test file that is named exactly like the detector, with the `_test` suffix. For example, for a `NodeRED_ExposedUI.textproto` you can find its `NodeRED_ExposedUI_test.textproto` counterpart. Once created, the file needs to contain a minimal configuration: ```proto config: { tested_plugin: "nameOfTheTestedPlugin" } ``` Where `nameOfTheTestedPlugin` is the name of the plugin. The `tested_plugin` configuration indicates to the test engine which detector to bind the test to. Without this option, your test will not work. You can additionally define the `disabled` configuration option, but in most cases you will not need to use it. ## Defining tests Once you have configured the general option for your unit tests, you need to actually define each tests. Most test will rely on a mock server to simulate the target application that we wrote a detector for. But before we dig deeper into mock servers, each test needs to have a name and whether the vulnerability will be found, for example: ```proto tests: { name: "whenVulnerable_returnsTrue" expect_vulnerability: true } tests: { name: "whenNotVulnerable_returnsFalse" expect_vulnerability: false } ``` See our [convention on naming tests](appendix-naming-tests). ## Using mock servers Now we can start simulating the behavior of our vulnerable application. Two mock capabilities are currently integrated in the templated plugin system: - An HTTP server; - A fake Callback server; Several mocks can be used at the same time. Let us take a simplified version of our previously defined plugin: ```proto actions: { name: "exploitation" http_request: { method: POST uri: "/exploit" data: "process=%{ import os; os.system('curl {{ T_CBS_URI }}') }%" response: { http_status: 200 } } } actions: { name: "check_callback_server_logs" callback_server: { action_type: CHECK } } workflows: { condition: REQUIRES_CALLBACK_SERVER actions: [ "exploitation", "check_callback_server_logs" ] } ``` To validate vulnerability detection of this plugin, we will need: - To simulate the callback server to return true. This can easily be done with the `mock_callback_server` directive; - To simulate the answer to `/exploit` with the HTTP server which can easily be done with the `mock_http_server` directive; ```proto tests: { name: "whenVulnerable_returnsTrue" expect_vulnerability: true mock_callback_server: { enabled: true has_interaction: true } mock_http_server: { mock_responses: [ { uri: "/exploit" status: 200 }, ] } } ``` And that is it. If we wanted to check a case where the server is not vulnerable we could use the following test case: ```proto tests: { name: "whenNotVulnerable_returnFalse" expect_vulnerability: false mock_http_server: { mock_responses: [ { uri: "/exploit" status: 403 }, ] } } ``` Note that we do not need the callback server anymore as the workflow will fail before. Additionally, the `/exploit` endpoint now returns a `403`. ## Adding a bit of magic to our world When mocking HTTP responses, Tsunami provides a few magic endpoints (to be used in the `uri` field). You can view them in the [glossary](glossary-tests-magic-uri). ## Generating things for you Finally, under the hood, Tsunami will also generate unit tests for you. This helps us detecting flaky detectors: for example when a detector fails to pass the "echo server" test (when the server is just repeating the request, a detector should not raise a vulnerability). ## Congratulations! Congratulations, you have finished writing your first templated plugin! ================================================ FILE: docs/howto/new-detector/templated/appendix-naming-actions.md ================================================ # Convention: How to name actions Actions must be named using the `[a-zA-Z0-9_]` character set. For example `this_is_my_action`. This naming convention helps improving discoverability of actions. ================================================ FILE: docs/howto/new-detector/templated/appendix-naming-plugin.md ================================================ # Convention: How to name a plugin The plugin name and filename should be identical as it makes for easier discoverability. Plugins should be named using the following convention: - All plugins should be named using the following character set: `[a-zA-Z0-9_]` so no spaces or special characters. - If the vulnerability has an associated CVE: `VulnerableApplicationName_CVE_YYYY_NNNNN` and the plugin should be placed in the `cve/YYYY/` directory. - If the vulnerability does not have an associated CVE: `VulnerableApplicationName_YYYY_VulnerabilityName`; if a vulnerability has no name you can try to describe it, for example `PreauthRCE`. The vulnerability will then be placed in the directory that matches the type of vulnerability, for example `rce/YYYY/VulnerableApplicationName_YYYY_VulnerabilityName`. - When the name of a plugin contains an acronym (e.g. `HTTP`, `UI`, `RCE`), that acronym must be in uppercase. ================================================ FILE: docs/howto/new-detector/templated/appendix-naming-tests.md ================================================ # How to name unit tests Use `condition_outcome` as the naming schema for your tests. That explicitly means that you should not prefix the test function name with "test". Example: `whenVulnerable_returnsTrue`. ================================================ FILE: docs/howto/new-detector/templated/appendix-using-linter.md ================================================ # Using the linter For all plugins written using our configuration format, we expect the plugins to be linted. The linter ensures that the plugin has the right format but also performs a series of checks that make sure it is behaving correctly. ## Installing the linter ### Using our docker image The linter is bundled in our docker image and will automatically run. ### Custom setup The linter is a Go binary that can very easily be installed: ``` $ go install github.com/google/tsunami-security-scanner-plugins/templated/utils/linter@latest $ linter ``` Note that depending on your current configuration, you might have to extend your `PATH`. See the [Golang documentation](https://go.dev/doc/tutorial/compile-install) for details. ================================================ FILE: docs/howto/new-detector/templated/glossary-predefined-variables.md ================================================ # Predefined variables Tsunami will provide a predefined set of variable to the environment that you can make use of in your actions. We try to maintain a strong naming convention for these : - `T_` stands for Tsunami and identifies a variable that is provided by the core engine; - `_UTL_` stands for utility and provides various utility variables; - `_NS_` stands for network service and provide information about the currently scanned network service; - `_CBS_` stands for callback server and provides information about the callback server. Here is the list of variables that are provided: - `T_UTL_CURRENT_TIMESTAMP_MS`: Provides the current timestamp in milliseconds. Note that the timestamp is computed at the beginning of a workflow run. It will thus be different between services but always return the same value within one run; - `T_NS_BASEURL`: The base URL of the network service being scanned. For example `http://127.0.0.1:9090` or `http://hostname.lan:1000`; - `T_NS_PROTOCOL`: The protocol used by the network service being scanned (e.g. `tcp`); - `T_NS_HOSTNAME`: The hostname of the network service being scanned. Note that this variable is only available if Tsunami was invoked with a hostname target (e.g. `hostname.lan`); - `T_NS_PORT`: The port of the network service being scanned (e.g. `1000`); - `T_NS_IP`: The IP of the network service being scanned (e.g. `127.0.0.1`); - `T_CBS_URI`: The callback server URL used to trigger the callback server. This is the main variable used when using the callback server. It contains the address and hashed secret (e.g. `http://tsunami-callback.lan/8fe7d878787d65` where `8fe7d878787d65` is the **hashed** secret); - `T_CBS_SECRET`: The callback server secret generated for the current workflow run; note that it is not hashed and is not relevant in most cases (e.g. `somesecret`); - `T_CBS_ADDRESS`: Address of the callback server (e.g. `tsunami-callback.lan`); - `T_CBS_PORT`: Port of the callback server (e.g. `80`); ================================================ FILE: docs/howto/new-detector/templated/glossary-tests-magic-uri.md ================================================ ## Magic tests URIs The following URIs are considered "magic" in tests when using the mock HTTP server: - `TSUNAMI_MAGIC_ANY_URI`: Will match any URI; so this answer will match any request; - `TSUNAMI_MAGIC_ECHO_SERVER`: Force Tsunami to repeat the request in the response. This is used internally to detect flaky detectors; ================================================ FILE: docs/howto/orchestration.md ================================================ # Tsunami Scan Orchestration ## Overview Tsunami follows a hardcoded 2-step process when scanning a publicly exposed network endpoint: * **Reconnaissance**: First, Tsunami identifies open ports and subsequently fingerprints protocols, services and other software running on the target host via a set of fingerprinting plugins. To not reinvent the wheel, Tsunami leverages existing tools such as [nmap](https://nmap.org/) for some of these tasks. * **Vulnerability verification**: Based on the information gathered in step 1, Tsunami selects all vulnerability verification plugins matching the identified services and executes them in order to verify vulnerabilities without false positives. ## Overall Scanning Workflow Following diagram shows the overall workflow for a Tsunami scan. ![orchestration](img/orchestration.svg) ## Reconnaissance In the reconnaissance step, Tsunami probes the scan target and gathers as much information about the scan target as possible, including: * open ports, * protocols, * network services & their banners, * potential software & corresponding version. Tsunami performs the Reconnaissance step in 2 separate phases. ### Port Scanning Phase In the port scanning phase, Tsunami performs port sweeping in order to identify open ports, protocols and network services on the scan target. The output of Port Scanning is a `PortScanReport` protobuf that contains all the identified `NetworkService`s from the port scanner. `PortScanner` is a special type of Tsunami plugins design for Port Scanning purpose. This allows users to swap the port scanning implementations. To not reinvent the wheel, users could choose a Tsunami plugin wrapper around existing tools like [nmap](https://nmap.org/) or [masscan](https://github.com/robertdavidgraham/masscan). You may find useful `PortScanner` implementations in [tsunami-security-scanner-plugins](https://github.com/google/tsunami-security-scanner-plugins/tree/master/google/portscan) repo. ### Fingerprinting Phase Usually port scanners only provide very basic service detection capability. When the scan target hosts complicated network services, like web servers, the scanner needs to perform further fingerprinting work to learn more about the exposed network services. For example, the scan target might choose to serve multiple web applications on the same TCP port 443 using nginx for reverse proxy, `/blog` for WordPress, and `/forum` for phpBB, etc. Port scanner will only be able to tell port 443 is running nginx. A Web Application Fingerprinter with a comprehensive crawler is required to identify these applications. `ServiceFingerprinter` is a special type of Tsunami plugin that allows users to define fingerprinters for a specific network service. By using filtering annotations, Tsunami will be able to automatically invoke appropriate `ServiceFingerprinter`s when it identifies matching network services. Tsunami only performs service fingerprinting for web services, using the [`WebServiceFingerprinter`](https://github.com/google/tsunami-security-scanner-plugins/blob/71c57f6bc151a3d97675d74c904a175172c77df4/google/fingerprinters/web/src/main/java/com/google/tsunami/plugins/fingerprinters/web/WebServiceFingerprinter.java) plugin. ### Reconnaissance Report At the end of the reconnaissance step, Tsunami compiles both the port scanner outputs and service fingerprinter outputs into a single `ReconnaissanceReport` protobuf for Vulnerability Verification. ## Vulnerability Verification In the Vulnerability Verification step, Tsunami executes the `VulnDetector` plugins in parallel to verify certain vulnerabilities on the scan target based on the information gathered in the Reconnaissance step. `VulnDetector`'s detection logic could either be implemented as plain Java code, or as a separate binary / script using a different language like python or go. External binaries and scripts have to be executed as separate processes outside of Tsunami using Tsunami's command execution util. ### Detector Selection Usually one `VulnDetector` only verifies one vulnerability and the vulnerability often only affects one type of network service or software. In order to avoid doing wasteful work, Tsunami allows plugins to be annotated by some filtering annotations to limit the scope of the plugin. Then before the Vulnerability Verification step starts, Tsunami will select matching `VulnDetector`s to run based on the exposed network services and running software on the scan target. Non-matching `VulnDetector`s will stay inactive throughout the entire scan. ================================================ FILE: docs/index.md ================================================ ================================================ FILE: full.Dockerfile ================================================ # Core engine FROM ghcr.io/google/tsunami-scanner-core:latest AS core # Callback server FROM ghcr.io/google/tsunami-security-scanner-callback-server:latest AS tcs # Plugins FROM ghcr.io/google/tsunami-plugins-google:latest AS plugins-google FROM ghcr.io/google/tsunami-plugins-templated:latest AS plugins-templated FROM ghcr.io/google/tsunami-plugins-doyensec:latest AS plugins-doyensec FROM ghcr.io/google/tsunami-plugins-community:latest AS plugins-community FROM ghcr.io/google/tsunami-plugins-govtech:latest AS plugins-govtech FROM ghcr.io/google/tsunami-plugins-facebook:latest AS plugins-facebook FROM ghcr.io/google/tsunami-plugins-python:latest AS plugins-python # Release a full version FROM ubuntu:latest AS release RUN apt-get update \ && apt-get install -y --no-install-recommends ca-certificates openjdk-21-jre golang python3 python3-venv \ && rm -rf /var/lib/apt/lists/* \ && rm -rf /usr/share/doc && rm -rf /usr/share/man \ && apt-get clean \ && mkdir logs/ COPY --from=core /usr/tsunami/ /usr/tsunami/ COPY --from=tcs /usr/tsunami/ /usr/tsunami/ COPY --from=plugins-google /usr/tsunami/plugins/ /usr/tsunami/plugins/ COPY --from=plugins-templated /usr/tsunami/plugins/ /usr/tsunami/plugins/ COPY --from=plugins-doyensec /usr/tsunami/plugins/ /usr/tsunami/plugins/ COPY --from=plugins-community /usr/tsunami/plugins/ /usr/tsunami/plugins/ COPY --from=plugins-govtech /usr/tsunami/plugins/ /usr/tsunami/plugins/ COPY --from=plugins-facebook /usr/tsunami/plugins/ /usr/tsunami/plugins/ COPY --from=plugins-python /usr/tsunami/py_plugins/ /usr/tsunami/py_plugins/ # Install the linter RUN go install github.com/google/tsunami-security-scanner-plugins/templated/utils/linter@latest \ && ln -s /root/go/bin/linter /usr/bin/tsunami-linter # Symlink the Python plugins so that they are discoverable by Python. RUN ln -s /usr/tsunami/py_plugins/ /usr/tsunami/py_server/py_plugins # Create the __init__.py files to ensure all plugins are discoverable. RUN find /usr/tsunami/py_plugins/ \ -type d \ ! -name '__pycache__' \ -exec touch '{}/__init__.py' \; # Create wrapper scripts WORKDIR /usr/tsunami RUN echo '#!/bin/bash\njava -cp /usr/tsunami/tsunami.jar:/usr/tsunami/plugins/* -Dtsunami.config.location=/usr/tsunami/tsunami.yaml com.google.tsunami.main.cli.TsunamiCli $*\n' > /usr/bin/tsunami \ && chmod +x /usr/bin/tsunami \ && echo '#!/bin/bash\njava -cp /usr/tsunami/tsunami-tcs.jar com.google.tsunami.callbackserver.main.TcsMain --custom-config=/usr/tsunami/tcs_config.yaml $*\n' > /usr/bin/tsunami-tcs \ && chmod +x /usr/bin/tsunami-tcs \ && echo '#!/bin/bash\n/usr/tsunami/py_venv/bin/python3 /usr/tsunami/py_server/plugin_server.py $*\n' > /usr/bin/tsunami-py-server \ && chmod +x /usr/bin/tsunami-py-server ================================================ FILE: go.mod ================================================ module github.com/google/tsunami-security-scanner go 1.22.0 ================================================ FILE: main/README.md ================================================ # Tsunami Main ## Overview This module provides the entry point for starting up Tsunami Security Scanner. ================================================ FILE: main/build.gradle ================================================ plugins { id 'application' id 'com.gradleup.shadow' version "8.3.6" } description = 'Tsunami: main' dependencies { implementation project(':tsunami-common') implementation project(':tsunami-plugin') implementation project(':tsunami-proto') implementation project(':tsunami-workflow') implementation "com.beust:jcommander:1.48" implementation "com.doyensec:libajp:1.0.0" implementation "com.google.cloud:google-cloud-storage:1.103.1" implementation "com.google.flogger:flogger:0.9" implementation "com.google.flogger:google-extensions:0.9" implementation "com.google.guava:guava:33.0.0-jre" implementation "com.google.inject:guice:6.0.0" implementation "com.google.protobuf:protobuf-java:3.25.5" implementation "io.github.classgraph:classgraph:4.8.65" implementation "io.grpc:grpc-netty:1.60.0" implementation "javax.inject:javax.inject:1" implementation "org.jsoup:jsoup:1.9.2" runtimeOnly "org.glassfish.jaxb:jaxb-runtime:2.3.1" testImplementation "com.google.truth:truth:1.4.4" testImplementation "com.google.truth.extensions:truth-java8-extension:1.4.4" testImplementation "com.google.truth.extensions:truth-proto-extension:1.4.4" testImplementation "junit:junit:4.13.2" testImplementation "org.mockito:mockito-core:5.18.0" } application { mainClassName = 'com.google.tsunami.main.cli.TsunamiCli' } shadowJar { exclude '*.proto' } tasks.named("distZip") { dependsOn(":tsunami-main:shadowJar") } tasks.named("distTar") { dependsOn(":tsunami-main:shadowJar") } tasks.named("startScripts") { dependsOn(":tsunami-main:shadowJar") } tasks.named("startShadowScripts") { dependsOn(":tsunami-main:jar") } tasks.named("compileJava") { dependsOn(":tsunami-plugin:shadowJar") } tasks.named('compileJava') { dependsOn(':tsunami-proto:shadowJar') dependsOn(':tsunami-workflow:shadowJar') } ================================================ FILE: main/src/main/java/com/google/tsunami/main/cli/LanguageServerOptions.java ================================================ /* * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.main.cli; import com.beust.jcommander.Parameter; import com.beust.jcommander.ParameterException; import com.beust.jcommander.Parameters; import com.google.common.collect.ImmutableList; import com.google.tsunami.common.cli.CliOption; import com.google.tsunami.common.data.NetworkEndpointUtils; import java.nio.file.Files; import java.nio.file.Paths; import java.util.List; /** Command line arguments for Tsunami language servers. */ @Parameters(separators = "=") public final class LanguageServerOptions implements CliOption { @Parameter( names = "--plugin-server-paths", description = "The filename of the language server to run language-specific plugins.") public List pluginServerFilenames = ImmutableList.of(); @Parameter( names = "--plugin-server-ports", description = "The port of the plugin server to open connection with. If not enough ports were" + " specified for the number of language servers specified, an open port will be" + " chosen.") public List pluginServerPorts = ImmutableList.of(); @Parameter( names = "--plugin-server-rpc-deadline-seconds", description = "The RPC deadline in seconds for the plugin servers.") public List pluginServerRpcDeadlineSeconds = ImmutableList.of(); @Parameter( names = {"--remote-plugin-server-addresses", "--python-plugin-server-address"}, description = "The address for remote language server (e.g. Python).") public List remotePluginServerAddress = ImmutableList.of(); @Parameter( names = {"--remote-plugin-server-ports", "--python-plugin-server-port"}, description = "The port of the remote plugin server to open connection with.") public List remotePluginServerPort = ImmutableList.of(); @Parameter( names = "--remote-plugin-server-rpc-deadline-seconds", description = "The RPC deadline in seconds for this plugin server.") public List remotePluginServerRpcDeadlineSeconds = ImmutableList.of(); @Override public void validate() { if (!pluginServerFilenames.isEmpty() || !pluginServerPorts.isEmpty()) { if (pluginServerFilenames != null && !pluginServerFilenames.isEmpty()) { for (String pluginServerFilename : pluginServerFilenames) { if (!Files.exists(Paths.get(pluginServerFilename))) { throw new ParameterException( String.format("Language server path %s does not exist", pluginServerFilename)); } } } if (pluginServerPorts != null && !pluginServerPorts.isEmpty()) { for (String pluginServerPort : pluginServerPorts) { try { int port = Integer.parseInt(pluginServerPort); if (!(port <= NetworkEndpointUtils.MAX_PORT_NUMBER && port > 0)) { throw new ParameterException( String.format( "Port out of range. Expected [0, %s], actual %s.", NetworkEndpointUtils.MAX_PORT_NUMBER, pluginServerPort)); } } catch (NumberFormatException e) { throw new ParameterException( String.format("Port number must be an integer. Got %s instead.", pluginServerPort), e); } } } var pathCounts = pluginServerFilenames == null ? 0 : pluginServerFilenames.size(); var portCounts = pluginServerPorts == null ? 0 : pluginServerPorts.size(); if (pathCounts != portCounts) { throw new ParameterException( String.format( "Number of plugin server paths must be equal to number of plugin server ports." + " Paths: %s. Ports: %s.", pathCounts, portCounts)); } if (!pluginServerRpcDeadlineSeconds.isEmpty()) { if (pluginServerRpcDeadlineSeconds.size() != pathCounts) { throw new ParameterException( String.format( "Number of plugin server rpc deadlines must be equal to number of plugin server" + " ports. Paths: %s. Ports: %s. Deadlines: %s", pathCounts, portCounts, pluginServerRpcDeadlineSeconds.size())); } } } if (!remotePluginServerAddress.isEmpty()) { var addrCounts = remotePluginServerAddress.size(); var portCounts = remotePluginServerPort.size(); if (addrCounts != portCounts) { throw new ParameterException( String.format( "Number of remote plugin server paths must be equal to number of plugin server " + "ports. Addresses: %s. Ports: %s.", addrCounts, portCounts)); } if (!remotePluginServerRpcDeadlineSeconds.isEmpty()) { if (remotePluginServerRpcDeadlineSeconds.size() != addrCounts) { throw new ParameterException( String.format( "Number of plugin server rpc deadlines must be equal to number of plugin server" + " ports. Paths: %s. Ports: %s. Deadlines: %s", addrCounts, portCounts, pluginServerRpcDeadlineSeconds.size())); } } for (int port : remotePluginServerPort) { if (!(port <= NetworkEndpointUtils.MAX_PORT_NUMBER && port > 0)) { throw new ParameterException( String.format( "Remote plugin server port out of range. Expected [0, %s], actual %s.", NetworkEndpointUtils.MAX_PORT_NUMBER, port)); } } } } } ================================================ FILE: main/src/main/java/com/google/tsunami/main/cli/ScanResultsArchiver.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.main.cli; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.tsunami.common.io.archiving.GoogleCloudStorageArchiver.GS_URL_PATTERN; import com.beust.jcommander.Parameter; import com.beust.jcommander.ParameterException; import com.beust.jcommander.Parameters; import com.google.cloud.storage.Storage; import com.google.cloud.storage.StorageOptions; import com.google.common.base.Strings; import com.google.common.flogger.GoogleLogger; import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.util.JsonFormat; import com.google.tsunami.common.cli.CliOption; import com.google.tsunami.common.io.archiving.Archiver; import com.google.tsunami.common.io.archiving.GoogleCloudStorageArchiver; import com.google.tsunami.common.io.archiving.RawFileArchiver; import com.google.tsunami.main.cli.option.OutputDataFormat; import com.google.tsunami.proto.ScanResults; import javax.inject.Inject; class ScanResultsArchiver { @Parameters(separators = "=") static final class Options implements CliOption { @Parameter( names = "--scan-results-local-output-filename", description = "The local output filename of the scanning results.") public String localOutputFilename; @Parameter( names = "--scan-results-local-output-format", description = "The format of the scanning results saved as local file.") public OutputDataFormat localOutputFormat; @Parameter( names = "--scan-results-gcs-output-file-url", description = "The GCS file url for the uploaded scanning results.") public String gcsOutputFileUrl; @Parameter( names = "--scan-results-gcs-output-format", description = "The format of the scanning results uploaded to GCS bucket.") public OutputDataFormat gcsOutputFormat; @Parameter( names = "--scan-results-logging-enabled", description = "Enable logging of the scan results.", arity = 1) public Boolean loggingEnabled = false; @Override public void validate() { if (!Strings.isNullOrEmpty(gcsOutputFileUrl) && !GS_URL_PATTERN.matcher(gcsOutputFileUrl).matches()) { throw new ParameterException(String.format("Malformed GCS URL: '%s'", gcsOutputFileUrl)); } } } private static final GoogleLogger logger = GoogleLogger.forEnclosingClass(); private final Options options; private final RawFileArchiver rawFileArchiver; private final GoogleCloudStorageArchiver.Factory googleCloudStorageArchiverFactory; @Inject // TODO(b/145315535): inject archivers using multibinder instead. ScanResultsArchiver( Options options, RawFileArchiver rawFileArchiver, GoogleCloudStorageArchiver.Factory googleCloudStorageArchiverFactory) { this.options = checkNotNull(options); this.rawFileArchiver = checkNotNull(rawFileArchiver); this.googleCloudStorageArchiverFactory = checkNotNull(googleCloudStorageArchiverFactory); } Storage getGcsStorage() { return StorageOptions.getDefaultInstance().getService(); } void archive(ScanResults scanResults) throws InvalidProtocolBufferException { if (!Strings.isNullOrEmpty(options.localOutputFilename)) { archive(rawFileArchiver, options.localOutputFilename, options.localOutputFormat, scanResults); } if (!Strings.isNullOrEmpty(options.gcsOutputFileUrl)) { GoogleCloudStorageArchiver archiver = googleCloudStorageArchiverFactory.create(getGcsStorage()); archive(archiver, options.gcsOutputFileUrl, options.gcsOutputFormat, scanResults); } if (options.loggingEnabled) { logger.atInfo().log("Scan results for RVD efficacy: %s", scanResults); } } private static void archive( Archiver archiver, String location, OutputDataFormat outputFormat, ScanResults scanResults) throws InvalidProtocolBufferException { switch (outputFormat) { case BIN_PROTO: archiver.archive(location, scanResults.toByteArray()); break; case JSON: archiver.archive(location, JsonFormat.printer().print(scanResults)); break; } } } ================================================ FILE: main/src/main/java/com/google/tsunami/main/cli/ScanResultsArchiverModule.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.main.cli; import com.google.inject.AbstractModule; /** Installs {@link ScanResultsArchiver}. */ final class ScanResultsArchiverModule extends AbstractModule { @Override protected void configure() { } } ================================================ FILE: main/src/main/java/com/google/tsunami/main/cli/TsunamiCli.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.main.cli; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.tsunami.common.data.NetworkEndpointUtils.forHostname; import static com.google.tsunami.common.data.NetworkEndpointUtils.forIp; import static com.google.tsunami.common.data.NetworkEndpointUtils.forIpAndHostname; import static com.google.tsunami.common.data.NetworkServiceUtils.buildUriNetworkService; import com.google.common.base.Stopwatch; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import com.google.common.flogger.GoogleLogger; import com.google.inject.AbstractModule; import com.google.inject.Guice; import com.google.inject.Injector; import com.google.tsunami.common.cli.CliOptionsModule; import com.google.tsunami.common.command.CommandExecutorModule; import com.google.tsunami.common.config.ConfigLoader; import com.google.tsunami.common.config.ConfigModule; import com.google.tsunami.common.config.TsunamiConfig; import com.google.tsunami.common.config.YamlConfigLoader; import com.google.tsunami.common.io.archiving.GoogleCloudStorageArchiverModule; import com.google.tsunami.common.net.http.HttpClientCliOptions; import com.google.tsunami.common.net.http.HttpClientModule; import com.google.tsunami.common.net.socket.TsunamiSocketFactoryModule; import com.google.tsunami.common.reflection.ClassGraphModule; import com.google.tsunami.common.server.LanguageServerCommand; import com.google.tsunami.common.time.SystemUtcClockModule; import com.google.tsunami.main.cli.option.MainCliOptions; import com.google.tsunami.main.cli.server.RemoteServerLoader; import com.google.tsunami.main.cli.server.RemoteServerLoaderModule; import com.google.tsunami.plugin.PluginExecutionModule; import com.google.tsunami.plugin.PluginLoadingModule; import com.google.tsunami.plugin.RemoteVulnDetectorLoadingModule; import com.google.tsunami.plugin.payload.PayloadGeneratorModule; import com.google.tsunami.proto.ScanResults; import com.google.tsunami.proto.ScanStatus; import com.google.tsunami.proto.ScanTarget; import com.google.tsunami.workflow.AdvisoriesWorkflow; import com.google.tsunami.workflow.DefaultScanningWorkflow; import com.google.tsunami.workflow.ScanningWorkflowException; import io.github.classgraph.ClassGraph; import io.github.classgraph.ScanResult; import java.io.IOException; import java.nio.file.Path; import java.security.SecureRandom; import java.time.Duration; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.ExecutionException; import javax.inject.Inject; /** Command line interface for the Tsunami Security Scanner. */ public final class TsunamiCli { private static final GoogleLogger logger = GoogleLogger.forEnclosingClass(); private final DefaultScanningWorkflow scanningWorkflow; private final AdvisoriesWorkflow advisoriesWorkflow; private final ScanResultsArchiver scanResultsArchiver; private final MainCliOptions mainCliOptions; private final RemoteServerLoader remoteServerLoader; @Inject TsunamiCli( DefaultScanningWorkflow scanningWorkflow, AdvisoriesWorkflow advisoriesWorkflow, ScanResultsArchiver scanResultsArchiver, MainCliOptions mainCliOptions, RemoteServerLoader remoteServerLoader) { this.scanningWorkflow = checkNotNull(scanningWorkflow); this.advisoriesWorkflow = checkNotNull(advisoriesWorkflow); this.scanResultsArchiver = checkNotNull(scanResultsArchiver); this.mainCliOptions = checkNotNull(mainCliOptions); this.remoteServerLoader = checkNotNull(remoteServerLoader); } public boolean run() throws ExecutionException, InterruptedException, ScanningWorkflowException, IOException { String logId = mainCliOptions.getLogId(); // TODO(b/171405612): Find a way to print the log ID at every log line. logger.atInfo().log("%sTsunamiCli starting...", logId); ImmutableList languageServerProcesses = remoteServerLoader.runServerProcesses(); if (mainCliOptions.dumpAdvisoriesPath != null && !mainCliOptions.dumpAdvisoriesPath.isEmpty()) { logger.atInfo().log("No scan will be performed. Dumping advisories."); advisoriesWorkflow.run(mainCliOptions.dumpAdvisoriesPath); return true; } ScanResults scanResults = scanningWorkflow.run(buildScanTarget()); languageServerProcesses.forEach(Process::destroy); logger.atInfo().log("Tsunami scan finished, saving results."); saveResults(scanResults); if (hasSuccessfulResults(scanResults)) { logger.atInfo().log("TsunamiCli finished..."); return true; } else { logger.atInfo().log( "Tsunami scan has failed status, message = %s.", scanResults.getStatusMessage()); return false; } } private static boolean hasSuccessfulResults(ScanResults scanResults) { return scanResults.getScanStatus().equals(ScanStatus.SUCCEEDED) || scanResults.getScanStatus().equals(ScanStatus.PARTIALLY_SUCCEEDED); } private ScanTarget buildScanTarget() { ScanTarget.Builder scanTargetBuilder = ScanTarget.newBuilder(); String ip = null; if (mainCliOptions.ipV4Target != null) { ip = mainCliOptions.ipV4Target; } else if (mainCliOptions.ipV6Target != null) { ip = mainCliOptions.ipV6Target; } if (ip != null && mainCliOptions.hostnameTarget != null) { scanTargetBuilder.setNetworkEndpoint(forIpAndHostname(ip, mainCliOptions.hostnameTarget)); } else if (ip != null) { scanTargetBuilder.setNetworkEndpoint(forIp(ip)); } else if (mainCliOptions.uriTarget != null) { scanTargetBuilder.setNetworkService(buildUriNetworkService(mainCliOptions.uriTarget)); } else { scanTargetBuilder.setNetworkEndpoint(forHostname(mainCliOptions.hostnameTarget)); } return scanTargetBuilder.build(); } private void saveResults(ScanResults scanResults) throws IOException { scanResultsArchiver.archive(scanResults); } private static final class TsunamiCliFirstStageModule extends AbstractModule { private final ScanResult classScanResult; private final String[] args; private final TsunamiConfig tsunamiConfig; TsunamiCliFirstStageModule( ScanResult classScanResult, String[] args, TsunamiConfig tsunamiConfig) { this.classScanResult = checkNotNull(classScanResult); this.args = checkNotNull(args); this.tsunamiConfig = checkNotNull(tsunamiConfig); } @Override protected void configure() { install(new ClassGraphModule(classScanResult)); install(new ConfigModule(classScanResult, tsunamiConfig)); install(new CliOptionsModule(classScanResult, "TsunamiCli", args)); } } private static final class TsunamiCliModule extends AbstractModule { private final ScanResult classScanResult; private final Injector parentInjector; private final TsunamiConfig tsunamiConfig; TsunamiCliModule( Injector parentInjector, ScanResult classScanResult, TsunamiConfig tsunamiConfig) { this.classScanResult = checkNotNull(classScanResult); this.parentInjector = checkNotNull(parentInjector); this.tsunamiConfig = checkNotNull(tsunamiConfig); } @Override protected void configure() { MainCliOptions mco = parentInjector.getInstance(MainCliOptions.class); LanguageServerOptions lso = parentInjector.getInstance(LanguageServerOptions.class); HttpClientCliOptions hcco = parentInjector.getInstance(HttpClientCliOptions.class); ScanResultsArchiver.Options srao = parentInjector.getInstance(ScanResultsArchiver.Options.class); ImmutableList commands = extractPluginServerArgs(mco, lso, hcco, srao); install(new SystemUtcClockModule()); install(new CommandExecutorModule()); install(new HttpClientModule.Builder().setLogId(mco.getLogId()).build()); install(new TsunamiSocketFactoryModule()); install(new GoogleCloudStorageArchiverModule()); install(new ScanResultsArchiverModule()); install(new PluginExecutionModule()); install(new PluginLoadingModule(classScanResult)); install(new PayloadGeneratorModule(new SecureRandom())); install(new RemoteServerLoaderModule(commands)); install(new RemoteVulnDetectorLoadingModule(commands)); } private ImmutableList extractPluginServerArgs( MainCliOptions mco, LanguageServerOptions lso, HttpClientCliOptions hcco, ScanResultsArchiver.Options srao) { List commands = Lists.newArrayList(); Boolean trustAllSslCertCli = hcco.trustAllCertificates; var logId = mco.getLogId(); var paths = lso.pluginServerFilenames; var ports = lso.pluginServerPorts; var rpcDeadline = lso.pluginServerRpcDeadlineSeconds; var remoteServerAddresses = lso.remotePluginServerAddress; var remoteServerPorts = lso.remotePluginServerPort; var remoteRpcDeadlines = lso.remotePluginServerRpcDeadlineSeconds; if (paths.isEmpty() && remoteServerAddresses.isEmpty()) { return ImmutableList.of(); } Map callbackConfig = tsunamiConfig.readConfigValue("plugin.callbackserver"); Map httpClientConfig = tsunamiConfig.readConfigValue("common.net.http"); boolean trustAllSslCertConfig = (boolean) httpClientConfig.getOrDefault("trust_all_certificates", false); String lngOutputDir = extractOutputDir(srao); boolean lngTrustAllSslCertCli = trustAllSslCertCli != null ? trustAllSslCertCli.booleanValue() : trustAllSslCertConfig; Duration lngConnectDuration = Duration.ofSeconds((int) httpClientConfig.getOrDefault("connect_timeout_seconds", 0)); String lngCallbackAddress = (String) callbackConfig.getOrDefault("callback_address", ""); Integer lngCallbackPort = (Integer) callbackConfig.getOrDefault("callback_port", 0); String lngPollingUri = (String) callbackConfig.getOrDefault("polling_uri", ""); for (int i = 0; i < paths.size(); ++i) { commands.add( LanguageServerCommand.create( paths.get(i), "", ports.get(i), logId, lngOutputDir, lngTrustAllSslCertCli, lngConnectDuration, lngCallbackAddress, lngCallbackPort, lngPollingUri, rpcDeadline.isEmpty() ? 0 : rpcDeadline.get(i))); } for (int i = 0; i < remoteServerAddresses.size(); ++i) { commands.add( LanguageServerCommand.create( "", remoteServerAddresses.get(i), remoteServerPorts.get(i).toString(), logId, lngOutputDir, lngTrustAllSslCertCli, lngConnectDuration, lngCallbackAddress, lngCallbackPort, lngPollingUri, remoteRpcDeadlines.isEmpty() ? 0 : remoteRpcDeadlines.get(i))); } return ImmutableList.copyOf(commands); } private String extractOutputDir(ScanResultsArchiver.Options sra) { if (!Strings.isNullOrEmpty(sra.localOutputFilename)) { return Path.of(sra.localOutputFilename).getParent().toString(); } return ""; } } public static int doMain(String[] args) { Stopwatch stopwatch = Stopwatch.createStarted(); TsunamiConfig tsunamiConfig = loadConfig(); try (ScanResult scanResult = new ClassGraph() .enableAllInfo() .blacklistPackages("com.google.tsunami.plugin.testing") .scan()) { logger.atInfo().log("Full classpath scan took %s", stopwatch); Injector firstStageInjector = Guice.createInjector(new TsunamiCliFirstStageModule(scanResult, args, tsunamiConfig)); Injector injector = firstStageInjector.createChildInjector( new TsunamiCliModule(firstStageInjector, scanResult, tsunamiConfig)); // Exit with non-zero code if scan failed. if (!injector.getInstance(TsunamiCli.class).run()) { return 1; } logger.atInfo().log("Full Tsunami scan took %s.", stopwatch.stop()); return 0; } catch (Throwable e) { logger.atSevere().withCause(e).log("Exiting due to workflow execution exceptions."); if (e instanceof InterruptedException) { Thread.currentThread().interrupt(); } return 1; } } public static void main(String[] args) { System.exit(doMain(args)); } private static TsunamiConfig loadConfig() { try (ScanResult scanResult = new ClassGraph().enableAllInfo().scan()) { ConfigLoader configLoader; Optional loaderClass = TsunamiConfig.getSystemProperty("tsunami.config.loader"); if (loaderClass.isPresent() && scanResult.getAllClassesAsMap().containsKey(loaderClass.get())) { configLoader = scanResult .getClassInfo(loaderClass.get()) .loadClass(ConfigLoader.class) .getConstructor() .newInstance(); } else { configLoader = new YamlConfigLoader(); } return configLoader.loadConfig(); } catch (ReflectiveOperationException e) { throw new LinkageError("Error loading config.", e); } } } ================================================ FILE: main/src/main/java/com/google/tsunami/main/cli/option/MainCliOptions.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.main.cli.option; import com.beust.jcommander.Parameter; import com.beust.jcommander.ParameterException; import com.beust.jcommander.Parameters; import com.google.tsunami.common.cli.CliOption; import com.google.tsunami.main.cli.option.validator.IpV4Validator; import com.google.tsunami.main.cli.option.validator.IpV6Validator; import java.util.ArrayList; import java.util.List; /** Command line arguments for Tsunami. */ @Parameters(separators = "=") public final class MainCliOptions implements CliOption { @Parameter( names = "--ip-v4-target", description = "The IP v4 address of the scanning target.", validateWith = IpV4Validator.class) public String ipV4Target; @Parameter( names = "--ip-v6-target", description = "The IP v6 address of the scanning target.", validateWith = IpV6Validator.class) public String ipV6Target; @Parameter(names = "--hostname-target", description = "The hostname of the scanning target.") public String hostnameTarget; @Parameter(names = "--log-id", description = "A log ID to print in front of the logs.") public String logId; @Parameter( names = "--uri-target", description = "The URI of the scanning target that supports both http & https schemes. When this" + " parameter is set, port scan is automatically skipped.") public String uriTarget; @Parameter( names = "--dump-advisories", description = "Disable scanning. Reports the list of currently enabled advisories to the specified" + " file, in textproto format.") public String dumpAdvisoriesPath; @Override public void validate() { if (dumpAdvisoriesPath != null && !dumpAdvisoriesPath.isEmpty()) { return; } List portScanEnabledTargets = new ArrayList<>(); List portScanDisabledTargets = new ArrayList<>(); if (ipV4Target != null) { portScanEnabledTargets.add("--ip-v4-target"); } if (ipV6Target != null) { portScanEnabledTargets.add("--ip-v6-target"); } if (hostnameTarget != null) { portScanEnabledTargets.add("--hostname-target"); } if (uriTarget != null) { portScanDisabledTargets.add("--uri-target"); } if (portScanEnabledTargets.isEmpty() && portScanDisabledTargets.isEmpty()) { throw new ParameterException( "One of the following parameters is expected: --ip-v4-target, --ip-v6-target," + " --hostname-target, --uri-target"); } if (!portScanEnabledTargets.isEmpty() && !portScanDisabledTargets.isEmpty()) { throw new ParameterException( "Parameters that require port scan (--ip-v4-target, --ip-v6-target, --hostname-target)" + " should not be passed along with parameters that skip port scan (--uri-target)"); } } /** Returns the log ID to print in front of the logs. */ public String getLogId() { return (logId == null) ? "" : (logId + ": "); } } ================================================ FILE: main/src/main/java/com/google/tsunami/main/cli/option/OutputDataFormat.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.main.cli.option; import com.google.common.base.Ascii; import java.util.Optional; /** Output format of Tsunami's scanning results. */ public enum OutputDataFormat { BIN_PROTO, JSON; /** * Parses the given {@code value} into {@link OutputDataFormat} enum. * * @param value the string representation of the {@link OutputDataFormat} enum. * @return the parsed {@link OutputDataFormat} enum. */ public static Optional parse(String value) { for (OutputDataFormat outputDataFormat : OutputDataFormat.values()) { if (Ascii.equalsIgnoreCase(outputDataFormat.name(), value)) { return Optional.of(outputDataFormat); } } return Optional.empty(); } } ================================================ FILE: main/src/main/java/com/google/tsunami/main/cli/option/validator/IpV4Validator.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.main.cli.option.validator; import java.net.Inet4Address; import java.net.InetAddress; /** Command line flag validator for an IP v4 address. */ public class IpV4Validator extends IpValidator { @Override protected int ipVersion() { return 4; } @Override protected boolean shouldAccept(InetAddress inetAddress) { return inetAddress instanceof Inet4Address; } } ================================================ FILE: main/src/main/java/com/google/tsunami/main/cli/option/validator/IpV6Validator.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.main.cli.option.validator; import java.net.Inet6Address; import java.net.InetAddress; /** Command line flag validator for an IP v6 address. */ public class IpV6Validator extends IpValidator { @Override protected int ipVersion() { return 6; } @Override protected boolean shouldAccept(InetAddress inetAddress) { return inetAddress instanceof Inet6Address; } } ================================================ FILE: main/src/main/java/com/google/tsunami/main/cli/option/validator/IpValidator.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.main.cli.option.validator; import com.beust.jcommander.IParameterValidator; import com.beust.jcommander.ParameterException; import com.google.common.base.Strings; import com.google.common.net.InetAddresses; import java.net.InetAddress; /** Base command line flag validator for an IP address. */ public abstract class IpValidator implements IParameterValidator { @Override public void validate(String name, String value) { if (Strings.isNullOrEmpty(value) || !InetAddresses.isInetAddress(value) || !shouldAccept(InetAddresses.forString(value))) { throw new ParameterException( String.format( "Parameter %s should point to a valid IP v%d address, got '%s'", name, ipVersion(), value)); } } protected abstract int ipVersion(); protected abstract boolean shouldAccept(InetAddress inetAddress); } ================================================ FILE: main/src/main/java/com/google/tsunami/main/cli/server/RemoteServerLoader.java ================================================ /* * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.main.cli.server; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.collect.ImmutableList.toImmutableList; import static java.lang.annotation.RetentionPolicy.RUNTIME; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.flogger.GoogleLogger; import com.google.tsunami.common.command.CommandExecutor; import com.google.tsunami.common.command.CommandExecutorFactory; import com.google.tsunami.common.server.LanguageServerCommand; import java.io.IOException; import java.lang.annotation.Retention; import java.util.List; import java.util.Optional; import java.util.concurrent.ExecutionException; import javax.inject.Inject; import javax.inject.Qualifier; /** Loader to run language servers. */ public class RemoteServerLoader { private static final GoogleLogger logger = GoogleLogger.forEnclosingClass(); private final List commands; @Inject RemoteServerLoader(@LanguageServerCommands List commands) { this.commands = checkNotNull(commands); } public ImmutableList runServerProcesses() { logger.atInfo().log("Starting language server processes (if any)..."); return commands.stream() // Filter out commands that don't need server start up .filter(command -> !Strings.isNullOrEmpty(command.serverCommand())) .map( command -> runProcess( CommandExecutorFactory.create( command.serverCommand(), getCommand("--port=", command.port()), getCommand("--log_id=", command.logId()), getCommand("--log_output=", command.outputDir()), "--trust_all_ssl_cert=" + command.trustAllSslCert(), getCommand("--timeout_seconds=", command.timeoutSeconds().getSeconds()), getCommand("--callback_address=", command.callbackAddress()), getCommand("--callback_port=", command.callbackPort()), getCommand("--polling_uri=", command.pollingUri())))) .filter(Optional::isPresent) .map(Optional::get) .collect(toImmutableList()); } private String getCommand(String flag, Object command) { return command.toString().isEmpty() || command.toString().equals("0") ? "" : flag + command; } private Optional runProcess(CommandExecutor executor) { try { return Optional.of(executor.executeAsync()); } catch (IOException | InterruptedException | ExecutionException e) { logger.atWarning().withCause(e).log("Could not execute language server binary."); } return Optional.empty(); } /** Guice interface for injecting {@link LanguageServerCommand} object lists. */ @Qualifier @Retention(RUNTIME) public @interface LanguageServerCommands {} } ================================================ FILE: main/src/main/java/com/google/tsunami/main/cli/server/RemoteServerLoaderModule.java ================================================ /* * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.main.cli.server; import com.google.common.collect.ImmutableList; import com.google.inject.AbstractModule; import com.google.inject.Provides; import com.google.tsunami.common.server.LanguageServerCommand; import java.util.List; /** Installs {@link RemoteServerLoaderModule}. */ public final class RemoteServerLoaderModule extends AbstractModule { private final ImmutableList commands; public RemoteServerLoaderModule(ImmutableList commands) { this.commands = commands; } @Provides @RemoteServerLoader.LanguageServerCommands List provideLanguageServerCommands() { return commands; } } ================================================ FILE: main/src/test/java/com/google/tsunami/main/cli/LanguageServerOptionsTest.java ================================================ /* * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.main.cli; import static org.junit.Assert.assertThrows; import com.beust.jcommander.ParameterException; import com.google.common.collect.ImmutableList; import com.google.tsunami.common.data.NetworkEndpointUtils; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @RunWith(JUnit4.class) public final class LanguageServerOptionsTest { @Test public void validate_whenPluginServerFilenameDoesNotExist_throwsParameterException() { LanguageServerOptions options = new LanguageServerOptions(); options.pluginServerFilenames = ImmutableList.of("nonexistingfile"); assertThrows(ParameterException.class, options::validate); } @Test public void validate_whenPortNumberNotInteger_throwsParameterException() { LanguageServerOptions options = new LanguageServerOptions(); options.pluginServerPorts = ImmutableList.of("test"); assertThrows(ParameterException.class, options::validate); } @Test public void validate_whenPortNumberOutOfRange_throwsParameterException() { LanguageServerOptions options = new LanguageServerOptions(); options.pluginServerPorts = ImmutableList.of("34567", "-1"); assertThrows( "Port out of range. Expected [0, " + NetworkEndpointUtils.MAX_PORT_NUMBER + "]" + ", actual -1", ParameterException.class, options::validate); } @Test public void validate_whenPythonPluginServerPortNumberOutOfRange_throwsParameterException() { LanguageServerOptions options = new LanguageServerOptions(); options.remotePluginServerAddress = ImmutableList.of("127.0.0.1"); options.remotePluginServerPort = ImmutableList.of(-1); assertThrows( "Remote plugin server port out of range. Expected [0, " + NetworkEndpointUtils.MAX_PORT_NUMBER + "]" + ", actual -1", ParameterException.class, options::validate); } @Test public void validate_whenPythonPluginServerInvalidNumberOfDeadlines_throwsParameterException() { LanguageServerOptions options = new LanguageServerOptions(); options.remotePluginServerAddress = ImmutableList.of("127.0.0.1"); options.remotePluginServerPort = ImmutableList.of(10000); options.remotePluginServerRpcDeadlineSeconds = ImmutableList.of(100, 200); assertThrows( "Number of plugin server rpc deadlines must be equal to number of plugin server. ports." + " Paths: 1. Ports: 1. Deadlines: 2", ParameterException.class, options::validate); } @Test public void validate_whenPythonPluginServerValidNumberOfDeadlines_succeeds() { LanguageServerOptions options = new LanguageServerOptions(); options.remotePluginServerAddress = ImmutableList.of("127.0.0.1"); options.remotePluginServerPort = ImmutableList.of(10000); options.remotePluginServerRpcDeadlineSeconds = ImmutableList.of(150); options.validate(); } } ================================================ FILE: main/src/test/java/com/google/tsunami/main/cli/ScanResultsArchiverTest.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.main.cli; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.spy; import com.beust.jcommander.ParameterException; import com.google.cloud.storage.Storage; import com.google.inject.AbstractModule; import com.google.inject.Guice; import com.google.inject.Provides; import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.util.JsonFormat; import com.google.tsunami.common.io.archiving.testing.FakeGoogleCloudStorageArchivers; import com.google.tsunami.common.io.archiving.testing.FakeGoogleCloudStorageArchiversModule; import com.google.tsunami.common.io.archiving.testing.FakeRawFileArchiver; import com.google.tsunami.common.io.archiving.testing.FakeRawFileArchiverModule; import com.google.tsunami.main.cli.option.OutputDataFormat; import com.google.tsunami.proto.ScanResults; import com.google.tsunami.proto.ScanStatus; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import javax.inject.Inject; import javax.inject.Qualifier; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; /** Tests for {@link ScanResultsArchiver}. */ @RunWith(JUnit4.class) public final class ScanResultsArchiverTest { private static final ScanResults SCAN_RESULTS = ScanResults.newBuilder().setScanStatus(ScanStatus.SUCCEEDED).build(); @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); @Mock Storage mockStorage; @Qualifier @Retention(RetentionPolicy.RUNTIME) private @interface SpyArchiver {} @Inject private FakeRawFileArchiver fakeRawFileArchiver; @Inject private FakeGoogleCloudStorageArchivers fakeGoogleCloudStorageArchivers; @Inject private ScanResultsArchiver.Options options; @Inject @SpyArchiver private ScanResultsArchiver scanResultsArchiver; @Before public void setUp() { Guice.createInjector( new AbstractModule() { @Override protected void configure() { bind(ScanResultsArchiver.Options.class) .toInstance(new ScanResultsArchiver.Options()); install(new ScanResultsArchiverModule()); install(new FakeRawFileArchiverModule()); install(new FakeGoogleCloudStorageArchiversModule()); } // TODO(b/145315535): wrap GCS API into a client library to get rid of this spy. @Provides @SpyArchiver ScanResultsArchiver getScanResultsArchiverSpy(ScanResultsArchiver delegate) { return spy(delegate); } }) .injectMembers(this); } @Test public void optionsValidate_whenInvalidGcsUrl_throwsParameterException() { options.gcsOutputFileUrl = "invalid_url"; assertThrows(ParameterException.class, options::validate); } @Test public void optionsValidate_defaultLoggingEnabled_isFalse() { assertThat(options.loggingEnabled).isFalse(); } @Test public void archive_withNoStorageEnabled_storesNothing() throws InvalidProtocolBufferException { options.localOutputFilename = ""; options.gcsOutputFileUrl = ""; scanResultsArchiver.archive(SCAN_RESULTS); fakeRawFileArchiver.assertNoDataStored(); fakeGoogleCloudStorageArchivers.assertNoDataStored(); } @Test public void archive_withLocalFileEnabledForJsonOutput_storesStringDataLocally() throws InvalidProtocolBufferException { options.localOutputFilename = "/tmp/result.json"; options.localOutputFormat = OutputDataFormat.JSON; options.gcsOutputFileUrl = ""; scanResultsArchiver.archive(SCAN_RESULTS); assertThat( parseJsonScanResults( fakeRawFileArchiver.getStoredCharSequence("/tmp/result.json").toString())) .isEqualTo(SCAN_RESULTS); fakeGoogleCloudStorageArchivers.assertNoDataStored(); } @Test public void archive_withLocalFileEnabledForBinProtoOutput_storesBytesDataLocally() throws InvalidProtocolBufferException { options.localOutputFilename = "/tmp/test.binproto"; options.localOutputFormat = OutputDataFormat.BIN_PROTO; options.gcsOutputFileUrl = ""; scanResultsArchiver.archive(SCAN_RESULTS); assertThat( ScanResults.parseFrom( fakeRawFileArchiver.getStoredByteArrays("/tmp/test.binproto"))) .isEqualTo(SCAN_RESULTS); fakeGoogleCloudStorageArchivers.assertNoDataStored(); } @Test public void archive_withGcsEnabledForJsonOutput_uploadsStringDataToGcs() throws InvalidProtocolBufferException { options.localOutputFilename = ""; options.gcsOutputFileUrl = "gs://test/object/result.json"; options.gcsOutputFormat = OutputDataFormat.JSON; doReturn(mockStorage).when(scanResultsArchiver).getGcsStorage(); scanResultsArchiver.archive(SCAN_RESULTS); assertThat( parseJsonScanResults( fakeGoogleCloudStorageArchivers .getStoredCharSequence(mockStorage, "gs://test/object/result.json") .toString())) .isEqualTo(SCAN_RESULTS); fakeRawFileArchiver.assertNoDataStored(); } @Test public void archive_withGcsEnabledForBinProtoOutput_uploadsBytesDataToGcs() throws InvalidProtocolBufferException { options.localOutputFilename = ""; options.gcsOutputFileUrl = "gs://test/object/result.binproto"; options.gcsOutputFormat = OutputDataFormat.BIN_PROTO; doReturn(mockStorage).when(scanResultsArchiver).getGcsStorage(); scanResultsArchiver.archive(SCAN_RESULTS); assertThat( ScanResults.parseFrom( fakeGoogleCloudStorageArchivers.getStoredByteArrays( mockStorage, "gs://test/object/result.binproto"))) .isEqualTo(SCAN_RESULTS); fakeRawFileArchiver.assertNoDataStored(); } @Test public void archive_withLocalAndGcsOptionEnabled_archivesToBothLocation() throws InvalidProtocolBufferException { options.localOutputFilename = "/tmp/result.json"; options.localOutputFormat = OutputDataFormat.JSON; options.gcsOutputFileUrl = "gs://test/object/result.binproto"; options.gcsOutputFormat = OutputDataFormat.BIN_PROTO; doReturn(mockStorage).when(scanResultsArchiver).getGcsStorage(); scanResultsArchiver.archive(SCAN_RESULTS); assertThat( parseJsonScanResults( fakeRawFileArchiver.getStoredCharSequence("/tmp/result.json").toString())) .isEqualTo(SCAN_RESULTS); assertThat( ScanResults.parseFrom( fakeGoogleCloudStorageArchivers.getStoredByteArrays( mockStorage, "gs://test/object/result.binproto"))) .isEqualTo(SCAN_RESULTS); } private static ScanResults parseJsonScanResults(String jsonScanResults) throws InvalidProtocolBufferException { ScanResults.Builder scanResultsBuilder = ScanResults.newBuilder(); JsonFormat.parser().merge(jsonScanResults, scanResultsBuilder); return scanResultsBuilder.build(); } } ================================================ FILE: main/src/test/java/com/google/tsunami/main/cli/TsunamiCliTest.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.main.cli; import static com.google.common.truth.Truth.assertThat; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.inject.AbstractModule; import com.google.inject.Guice; import com.google.tsunami.common.cli.CliOptionsModule; import com.google.tsunami.common.config.ConfigModule; import com.google.tsunami.common.config.TsunamiConfig; import com.google.tsunami.common.data.NetworkEndpointUtils; import com.google.tsunami.common.net.http.HttpClientModule; import com.google.tsunami.common.time.testing.FakeUtcClockModule; import com.google.tsunami.main.cli.server.RemoteServerLoaderModule; import com.google.tsunami.plugin.payload.PayloadGeneratorModule; import com.google.tsunami.plugin.testing.FailedVulnDetectorBootstrapModule; import com.google.tsunami.plugin.testing.FakePluginExecutionModule; import com.google.tsunami.plugin.testing.FakePortScanner; import com.google.tsunami.plugin.testing.FakePortScannerBootstrapModule; import com.google.tsunami.plugin.testing.FakePortScannerBootstrapModule2; import com.google.tsunami.plugin.testing.FakeServiceFingerprinter; import com.google.tsunami.plugin.testing.FakeServiceFingerprinterBootstrapModule; import com.google.tsunami.plugin.testing.FakeVulnDetector; import com.google.tsunami.plugin.testing.FakeVulnDetector2; import com.google.tsunami.plugin.testing.FakeVulnDetectorBootstrapModule; import com.google.tsunami.plugin.testing.FakeVulnDetectorBootstrapModule2; import com.google.tsunami.proto.AddressFamily; import com.google.tsunami.proto.DetectionReport; import com.google.tsunami.proto.Hostname; import com.google.tsunami.proto.IpAddress; import com.google.tsunami.proto.NetworkEndpoint; import com.google.tsunami.proto.NetworkService; import com.google.tsunami.proto.Port; import com.google.tsunami.proto.ReconnaissanceReport; import com.google.tsunami.proto.ScanFinding; import com.google.tsunami.proto.ScanResults; import com.google.tsunami.proto.ScanStatus; import com.google.tsunami.proto.ServiceContext; import com.google.tsunami.proto.TargetInfo; import com.google.tsunami.proto.TransportProtocol; import com.google.tsunami.proto.WebServiceContext; import com.google.tsunami.workflow.ScanningWorkflowException; import io.github.classgraph.ClassGraph; import io.github.classgraph.ScanResult; import java.io.File; import java.io.IOException; import java.net.Inet4Address; import java.net.InetAddress; import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import java.security.SecureRandom; import java.util.concurrent.ExecutionException; import java.util.stream.Stream; import javax.inject.Inject; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; /** Tests for {@link TsunamiCli}. */ @RunWith(JUnit4.class) public final class TsunamiCliTest { private static final String IP_TARGET = "127.0.0.1"; private static final String HOSTNAME_TARGET = "localhost"; private static final String URI_TARGET = "https://localhost/function1"; @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); @Rule public TemporaryFolder tempFolder = new TemporaryFolder(); @Mock ScanResultsArchiver scanResultsArchiver; @Captor ArgumentCaptor scanResultsCaptor; @Inject private TsunamiCli tsunamiCli; private boolean runCli(ImmutableMap rawConfigData, String... args) throws InterruptedException, ExecutionException, ScanningWorkflowException, IOException { try (ScanResult scanResult = new ClassGraph().enableAllInfo().scan()) { Guice.createInjector( new AbstractModule() { @Override protected void configure() { bind(ScanResultsArchiver.class).toInstance(scanResultsArchiver); install(new HttpClientModule.Builder().build()); install(new PayloadGeneratorModule(new SecureRandom())); install(new ConfigModule(scanResult, TsunamiConfig.fromYamlData(rawConfigData))); install(new CliOptionsModule(scanResult, "TsunamiCliTest", args)); install(new FakeUtcClockModule()); install(new FakePluginExecutionModule()); install(new FakePortScannerBootstrapModule()); install(new FakePortScannerBootstrapModule2()); install(new FakeServiceFingerprinterBootstrapModule()); install(new FakeVulnDetectorBootstrapModule()); install(new FakeVulnDetectorBootstrapModule2()); install(new RemoteServerLoaderModule(ImmutableList.of())); } }) .injectMembers(this); return tsunamiCli.run(); } } @Test public void run_whenIpTarget_generatesAndArchivesCorrectResult() throws InterruptedException, ExecutionException, ScanningWorkflowException, IOException { NetworkService expectedNetworkService = FakeServiceFingerprinter.addWebServiceContext( FakePortScanner.getFakeNetworkService(NetworkEndpointUtils.forIp(IP_TARGET))); boolean scanSucceeded = runCli(ImmutableMap.of(), "--ip-v4-target=" + IP_TARGET); assertThat(scanSucceeded).isTrue(); TargetInfo targetInfo = TargetInfo.newBuilder().addNetworkEndpoints(NetworkEndpointUtils.forIp(IP_TARGET)).build(); verify(scanResultsArchiver, times(1)).archive(scanResultsCaptor.capture()); ScanResults storedScanResult = scanResultsCaptor.getValue(); assertThat(storedScanResult.getScanStatus()).isEqualTo(ScanStatus.SUCCEEDED); assertThat(storedScanResult.getScanFindingsList()) .containsExactlyElementsIn( Stream.of( FakeVulnDetector.getFakeDetectionReport(targetInfo, expectedNetworkService), FakeVulnDetector2.getFakeDetectionReport(targetInfo, expectedNetworkService)) .map(TsunamiCliTest::buildScanFindingFromDetectionReport) .toArray()); assertThat(storedScanResult.getReconnaissanceReport()) .isEqualTo( ReconnaissanceReport.newBuilder() .setTargetInfo( TargetInfo.newBuilder() .addNetworkEndpoints(NetworkEndpointUtils.forIp(IP_TARGET))) .addNetworkServices( FakeServiceFingerprinter.addWebServiceContext( FakePortScanner.getFakeNetworkService( NetworkEndpointUtils.forIp(IP_TARGET)))) .build()); } @Test public void run_whenHostnameTarget_generatesAndArchivesCorrectResult() throws InterruptedException, ExecutionException, ScanningWorkflowException, IOException { NetworkService expectedNetworkService = FakeServiceFingerprinter.addWebServiceContext( FakePortScanner.getFakeNetworkService( NetworkEndpointUtils.forHostname(HOSTNAME_TARGET))); boolean scanSucceeded = runCli(ImmutableMap.of(), "--hostname-target=" + HOSTNAME_TARGET); assertThat(scanSucceeded).isTrue(); TargetInfo targetInfo = TargetInfo.newBuilder() .addNetworkEndpoints(NetworkEndpointUtils.forHostname(HOSTNAME_TARGET)) .build(); verify(scanResultsArchiver, times(1)).archive(scanResultsCaptor.capture()); ScanResults storedScanResult = scanResultsCaptor.getValue(); assertThat(storedScanResult.getScanStatus()).isEqualTo(ScanStatus.SUCCEEDED); assertThat(storedScanResult.getScanFindingsList()) .containsExactlyElementsIn( Stream.of( FakeVulnDetector.getFakeDetectionReport(targetInfo, expectedNetworkService), FakeVulnDetector2.getFakeDetectionReport(targetInfo, expectedNetworkService)) .map(TsunamiCliTest::buildScanFindingFromDetectionReport) .toArray()); assertThat(storedScanResult.getReconnaissanceReport()) .isEqualTo( ReconnaissanceReport.newBuilder() .setTargetInfo( TargetInfo.newBuilder() .addNetworkEndpoints(NetworkEndpointUtils.forHostname(HOSTNAME_TARGET))) .addNetworkServices( FakeServiceFingerprinter.addWebServiceContext( FakePortScanner.getFakeNetworkService( NetworkEndpointUtils.forHostname(HOSTNAME_TARGET)))) .build()); } @Test public void run_whenUriTarget_generatesCorrectResult() throws InterruptedException, ExecutionException, IOException { boolean scanSucceeded = runCli(ImmutableMap.of(), "--uri-target=" + URI_TARGET); assertThat(scanSucceeded).isTrue(); URL url = new URL(URI_TARGET); String hostname = url.getHost(); String ipaddress = InetAddress.getByName(hostname).getHostAddress(); InetAddress inetAddress = InetAddress.getByName(url.getHost()); AddressFamily addressFamily = inetAddress instanceof Inet4Address ? AddressFamily.IPV4 : AddressFamily.IPV6; NetworkEndpoint networkEndpoint = NetworkEndpoint.newBuilder() .setType(NetworkEndpoint.Type.IP_HOSTNAME_PORT) .setHostname(Hostname.newBuilder().setName("localhost")) .setPort(Port.newBuilder().setPortNumber(443)) .setIpAddress( IpAddress.newBuilder().setAddressFamily(addressFamily).setAddress(ipaddress)) .build(); verify(scanResultsArchiver, times(1)).archive(scanResultsCaptor.capture()); ScanResults storedScanResult = scanResultsCaptor.getValue(); assertThat(storedScanResult.getScanStatus()).isEqualTo(ScanStatus.SUCCEEDED); assertThat(storedScanResult.getReconnaissanceReport()) .isEqualTo( ReconnaissanceReport.newBuilder() .setTargetInfo(TargetInfo.newBuilder().addNetworkEndpoints(networkEndpoint)) .addNetworkServices( NetworkService.newBuilder() .setNetworkEndpoint(networkEndpoint) .setTransportProtocol(TransportProtocol.TCP) .setServiceName("https") .setServiceContext( ServiceContext.newBuilder() .setWebServiceContext( WebServiceContext.newBuilder() .setApplicationRoot(url.getPath())))) .build()); } @Test public void run_whenIpAndHostnameTarget_generatesCorrectResult() throws InterruptedException, ExecutionException, IOException { boolean scanSucceeded = runCli( ImmutableMap.of(), "--ip-v4-target=" + IP_TARGET, "--hostname-target=" + HOSTNAME_TARGET); assertThat(scanSucceeded).isTrue(); verify(scanResultsArchiver, times(1)).archive(scanResultsCaptor.capture()); ScanResults storedScanResult = scanResultsCaptor.getValue(); assertThat(storedScanResult.getScanStatus()).isEqualTo(ScanStatus.SUCCEEDED); assertThat(storedScanResult.getReconnaissanceReport()) .isEqualTo( ReconnaissanceReport.newBuilder() .setTargetInfo( TargetInfo.newBuilder() .addNetworkEndpoints( NetworkEndpointUtils.forIpAndHostname(IP_TARGET, HOSTNAME_TARGET))) .addNetworkServices( FakeServiceFingerprinter.addWebServiceContext( FakePortScanner.getFakeNetworkService( NetworkEndpointUtils.forIpAndHostname(IP_TARGET, HOSTNAME_TARGET)))) .build()); } @Test public void run_whenScanFailed_generatesFailedScanResults() throws InterruptedException, ExecutionException, IOException { try (ScanResult scanResult = new ClassGraph().enableAllInfo().scan()) { Guice.createInjector( new AbstractModule() { @Override protected void configure() { bind(ScanResultsArchiver.class).toInstance(scanResultsArchiver); install(new HttpClientModule.Builder().build()); install(new PayloadGeneratorModule(new SecureRandom())); install( new ConfigModule(scanResult, TsunamiConfig.fromYamlData(ImmutableMap.of()))); install( new CliOptionsModule( scanResult, "TsunamiCliTest", new String[] { "--ip-v4-target=" + IP_TARGET, "--hostname-target=" + HOSTNAME_TARGET })); install(new FakeUtcClockModule()); install(new FakePluginExecutionModule()); install(new FakePortScannerBootstrapModule()); install(new FailedVulnDetectorBootstrapModule()); install(new RemoteServerLoaderModule(ImmutableList.of())); } }) .injectMembers(this); boolean scanSucceeded = tsunamiCli.run(); assertThat(scanSucceeded).isFalse(); verify(scanResultsArchiver, times(1)).archive(scanResultsCaptor.capture()); ScanResults storedScanResult = scanResultsCaptor.getValue(); assertThat(storedScanResult.getScanStatus()).isEqualTo(ScanStatus.FAILED); assertThat(storedScanResult.getStatusMessage()).isEqualTo("All VulnDetectors failed."); } } @Test public void run_whenAdvisoryMode_generatesAdvisories() throws InterruptedException, ExecutionException, ScanningWorkflowException, IOException { File tempFile = tempFolder.newFile("advisories.csv"); Path tempPath = tempFile.toPath(); boolean scanSucceeded = runCli(ImmutableMap.of(), "--dump-advisories=" + tempPath.toString()); String advisories = Files.readString(tempPath); String expectedAdvisories = """ vulnerabilities { main_id { publisher: "GOOGLE" value: "FakeVuln1" } severity: CRITICAL title: "FakeTitle1" description: "FakeDescription1" } vulnerabilities { main_id { publisher: "GOOGLE" value: "FakeVuln2" } severity: MEDIUM title: "FakeTitle2" description: "FakeDescription2" } """; assertThat(scanSucceeded).isTrue(); assertThat(advisories).isEqualTo(expectedAdvisories); } private static ScanFinding buildScanFindingFromDetectionReport(DetectionReport detectionReport) { return ScanFinding.newBuilder() .setTargetInfo(detectionReport.getTargetInfo()) .setNetworkService(detectionReport.getNetworkService()) .setVulnerability(detectionReport.getVulnerability()) .build(); } } ================================================ FILE: main/src/test/java/com/google/tsunami/main/cli/option/MainCliOptionsTest.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.main.cli.option; import static org.junit.Assert.assertThrows; import com.beust.jcommander.ParameterException; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** Tests for {@link MainCliOptions}. */ @RunWith(JUnit4.class) public class MainCliOptionsTest { @Test public void validate_whenDumpAdvisoriesPathPassed_doesNotThrowParameterException() { MainCliOptions cliOptions = new MainCliOptions(); cliOptions.dumpAdvisoriesPath = "path/to/dump/advisories"; cliOptions.validate(); } @Test public void validate_whenMissingScanTarget_throwsParameterException() { MainCliOptions cliOptions = new MainCliOptions(); assertThrows(ParameterException.class, cliOptions::validate); } @Test public void validate_whenUriTargetPassedWithHostnameTarget_throwsParameterException() { MainCliOptions cliOptions = new MainCliOptions(); cliOptions.hostnameTarget = "localhost"; cliOptions.uriTarget = "https://localhost/function1"; assertThrows(ParameterException.class, cliOptions::validate); } } ================================================ FILE: main/src/test/java/com/google/tsunami/main/cli/option/OutputDataFormatTest.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.main.cli.option; import static com.google.common.truth.Truth.assertThat; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** Tests for {@link OutputDataFormat}. */ @RunWith(JUnit4.class) public class OutputDataFormatTest { @Test public void parse_whenStringMatchesExactly_returnsParsedOutputDataFormat() { assertThat(OutputDataFormat.parse("BIN_PROTO")).hasValue(OutputDataFormat.BIN_PROTO); assertThat(OutputDataFormat.parse("JSON")).hasValue(OutputDataFormat.JSON); } @Test public void parse_whenStringMatchesIgnoringCases_returnsParsedOutputDataFormat() { assertThat(OutputDataFormat.parse("bin_proto")).hasValue(OutputDataFormat.BIN_PROTO); assertThat(OutputDataFormat.parse("Json")).hasValue(OutputDataFormat.JSON); } @Test public void parse_whenStringNotMatch_returnsEmpty() { assertThat(OutputDataFormat.parse("xml")).isEmpty(); } } ================================================ FILE: main/src/test/java/com/google/tsunami/main/cli/option/validator/IpV4ValidatorTest.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.main.cli.option.validator; import com.google.common.collect.ImmutableList; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** Tests for {@link IpV4Validator}. */ @RunWith(JUnit4.class) public class IpV4ValidatorTest extends IpValidatorTest { @Override protected String flagName() { return "ip-v4-target"; } @Override protected IpValidator getValidator() { return new IpV4Validator(); } @Override protected ImmutableList validIps() { return ImmutableList.of("127.0.0.1", "8.8.8.8"); } @Override protected ImmutableList invalidIps() { return ImmutableList.of("", "bogus_string", "1234", "2002:af4:9b91::", "www.google.com"); } } ================================================ FILE: main/src/test/java/com/google/tsunami/main/cli/option/validator/IpV6ValidatorTest.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.main.cli.option.validator; import com.google.common.collect.ImmutableList; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** Tests for {@link IpV6Validator}. */ @RunWith(JUnit4.class) public class IpV6ValidatorTest extends IpValidatorTest { @Override protected String flagName() { return "ip-v6-target"; } @Override protected IpValidator getValidator() { return new IpV6Validator(); } @Override protected ImmutableList validIps() { return ImmutableList.of( "0:0:0:0:0:0:0:1", "fe80::a", "fe80::1", "fe80::2", "fe80::42", "fe80::3dd0:7f8e:57b7:34d5", "fe80:3dd0:7f8e:57b7:0:0:0:0", "::4:0:0:0:ffff", "0:0:3::ffff", "7::0.128.0.127"); } @Override protected ImmutableList invalidIps() { return ImmutableList.of( "", "bogus_string", "1234", "127.0.0.1", "www.google.com", "[1:2e]", "[fe80:a"); } } ================================================ FILE: main/src/test/java/com/google/tsunami/main/cli/option/validator/IpValidatorTest.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.main.cli.option.validator; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; import com.beust.jcommander.ParameterException; import com.google.common.collect.ImmutableList; import org.junit.Test; /** Base class for IP validators. */ public abstract class IpValidatorTest { @Test public void validate_withValidIpValue_doesNotThrows() { for (String validIp : validIps()) { try { getValidator().validate(flagName(), validIp); } catch (ParameterException e) { throw new AssertionError("Unexpected ParameterException for IP: " + validIp, e); } } } @Test public void validate_withInvalidIpValue_throwsParameterException() { for (String invalidIp : invalidIps()) { ParameterException exception = assertThrows( ParameterException.class, () -> getValidator().validate(flagName(), invalidIp)); assertThat(exception) .hasMessageThat() .isEqualTo( String.format( "Parameter %s should point to a valid IP v%d address, got '%s'", flagName(), getValidator().ipVersion(), invalidIp)); } } protected abstract String flagName(); protected abstract IpValidator getValidator(); protected abstract ImmutableList validIps(); protected abstract ImmutableList invalidIps(); } ================================================ FILE: main/src/test/java/com/google/tsunami/main/cli/server/RemoteServerLoaderTest.java ================================================ /* * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.main.cli.server; import static com.google.common.truth.Truth.assertThat; import com.google.common.collect.ImmutableList; import com.google.inject.Guice; import com.google.tsunami.common.server.LanguageServerCommand; import java.time.Duration; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @RunWith(JUnit4.class) public final class RemoteServerLoaderTest { @Test public void runServerProcess_whenPathExistsAndNormalPort_returnsValidProcessList() { ImmutableList commands = ImmutableList.of( LanguageServerCommand.create( "/bin/sh", "", "34567", "34", "/output-here", false, Duration.ofSeconds(10), "157.34.0.2", 8080, "157.34.0.2:8881", 0)); RemoteServerLoader loader = Guice.createInjector(new RemoteServerLoaderModule(commands)) .getInstance(RemoteServerLoader.class); var processList = loader.runServerProcesses(); assertThat(processList).hasSize(1); assertThat(processList.get(0)).isNotNull(); } @Test public void runServerProcess_whenServerAddressExistsAndNormalPort_returnsEmptyProcessList() { ImmutableList commands = ImmutableList.of( LanguageServerCommand.create( "", "127.0.0.1", "34567", "34", "/output-here", false, Duration.ofSeconds(10), "157.34.0.2", 8080, "157.34.0.2:8881", 0)); RemoteServerLoader loader = Guice.createInjector(new RemoteServerLoaderModule(commands)) .getInstance(RemoteServerLoader.class); var processList = loader.runServerProcesses(); assertThat(processList).isEmpty(); } } ================================================ FILE: plugin/README.md ================================================ # Tsunami Plugin Module ## Overview This module provides plugin development and management APIs for Tsunami Security Scanner. ================================================ FILE: plugin/build.gradle ================================================ description = 'Tsunami: Plugin' def tcsRepoBranch = System.getenv("GITBRANCH_TSUNAMI_TCS") ?: "stable" dependencies { implementation project(':tsunami-common') implementation project(':tsunami-proto') implementation("com.google.tsunami:tcs-common") { version { branch = "${tcsRepoBranch}" } } implementation("com.google.tsunami:tcs-proto") { version { branch = "${tcsRepoBranch}" } } implementation "com.beust:jcommander:1.48" implementation "com.google.auto.value:auto-value-annotations:1.11.0" implementation "com.google.code.gson:gson:2.10.1" implementation "com.google.flogger:flogger:0.9" implementation "com.google.flogger:google-extensions:0.9" implementation "com.google.guava:guava:33.0.0-jre" implementation "com.google.http-client:google-http-client:1.44.1" implementation "com.google.inject:guice:6.0.0" implementation "com.google.protobuf:protobuf-java-util:3.25.5" implementation "com.google.protobuf:protobuf-java:3.25.5" implementation "com.squareup.okhttp3:mockwebserver:3.12.0" implementation "io.github.classgraph:classgraph:4.8.65" implementation "io.grpc:grpc-context:1.60.0" implementation "io.grpc:grpc-core:1.60.0" implementation "io.grpc:grpc-netty:1.60.0" implementation "io.grpc:grpc-services:1.60.0" implementation "io.grpc:grpc-testing:1.60.0" implementation "javax.inject:javax.inject:1" implementation "org.yaml:snakeyaml:1.26" annotationProcessor "com.google.auto.value:auto-value:1.10.4" testImplementation "com.google.guava:guava-testlib:33.0.0-jre" testImplementation "com.google.truth:truth:1.4.4" testImplementation "com.google.truth.extensions:truth-java8-extension:1.4.4" testImplementation "com.google.truth.extensions:truth-proto-extension:1.4.4" testImplementation "com.squareup.okhttp3:mockwebserver:3.12.0" testImplementation "junit:junit:4.13.2" } tasks.named("compileJava") { dependsOn(":tsunami-common:shadowJar") } ================================================ FILE: plugin/src/main/java/com/google/tsunami/plugin/LanguageServerException.java ================================================ /* * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.plugin; import com.google.tsunami.common.ErrorCode; import com.google.tsunami.common.TsunamiException; /** Exception for language server errors. */ public final class LanguageServerException extends TsunamiException { public LanguageServerException(String message) { super(ErrorCode.LANGUAGE_SERVER_ERROR, message); } public LanguageServerException(String message, Throwable cause) { super(ErrorCode.LANGUAGE_SERVER_ERROR, message, cause); } } ================================================ FILE: plugin/src/main/java/com/google/tsunami/plugin/PluginBootstrapModule.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.plugin; import static com.google.common.base.Preconditions.checkNotNull; import com.google.common.flogger.GoogleLogger; import com.google.inject.AbstractModule; import com.google.inject.multibindings.MapBinder; /** * Base class for bootstrapping a {@link TsunamiPlugin}. * *

A valid {@link PluginBootstrapModule} subclass must be defined for each {@link TsunamiPlugin}. * This is enforced by the {@link com.google.tsunami.plugin.annotations.PluginInfo} annotation. */ public abstract class PluginBootstrapModule extends AbstractModule { private static final GoogleLogger logger = GoogleLogger.forEnclosingClass(); private MapBinder tsunamiPluginBinder; @Override protected final void configure() { tsunamiPluginBinder = MapBinder.newMapBinder(binder(), PluginDefinition.class, TsunamiPlugin.class); configurePlugin(); } /** * All bootstrapping logic for a {@link TsunamiPlugin} should be implemented in this method. * {@link PluginBootstrapModule#registerPlugin(Class)} must also be called in order to register * the plugin to Tsunami. */ protected abstract void configurePlugin(); /** * Register a {@link TsunamiPlugin} to Tsunami's plugin module using Guice's multibinding feature. * * @param tsunamiPluginClazz the {@link Class} for the {@link TsunamiPlugin} to be registered. */ protected final void registerPlugin(Class tsunamiPluginClazz) { checkNotNull(tsunamiPluginClazz); tsunamiPluginBinder .addBinding(PluginDefinition.forPlugin(tsunamiPluginClazz)) .to(tsunamiPluginClazz); logger.atInfo().log("Plugin %s is registered.", tsunamiPluginClazz); } /** * Register a {@link TsunamiPlugin} that is dynamically created at runtime. * * @param name the name of the plugin * @param author the author of the plugin * @param requiresCallbackServer whether the plugin requires a callback server * @param tsunamiPlugin the {@link TsunamiPlugin} to be registered */ protected final void registerDynamicPlugin( PluginType pluginType, String name, String author, boolean requiresCallbackServer, boolean isForWebService, TsunamiPlugin tsunamiPlugin) { var pluginDef = PluginDefinition.forDynamicPlugin( pluginType, name, author, isForWebService, requiresCallbackServer); tsunamiPluginBinder.addBinding(pluginDef).toInstance(tsunamiPlugin); logger.atInfo().log("Dynamic plugin registered: %s", pluginDef.name()); } } ================================================ FILE: plugin/src/main/java/com/google/tsunami/plugin/PluginDefinition.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.plugin; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; import com.google.auto.value.AutoValue; import com.google.auto.value.extension.memoized.Memoized; import com.google.tsunami.plugin.annotations.ForOperatingSystemClass; import com.google.tsunami.plugin.annotations.ForServiceName; import com.google.tsunami.plugin.annotations.ForSoftware; import com.google.tsunami.plugin.annotations.ForWebService; import com.google.tsunami.plugin.annotations.PluginInfo; import com.google.tsunami.plugin.annotations.RequiresCallbackServer; import java.util.Optional; /** A data class that captures all the definition details about a {@link TsunamiPlugin}. */ @AutoValue abstract class PluginDefinition { abstract PluginType type(); abstract String name(); abstract String author(); abstract String version(); abstract Optional targetServiceName(); abstract Optional targetSoftware(); abstract boolean isForWebService(); abstract Optional targetOperatingSystemClass(); abstract boolean requiresCallbackServer(); /** * Unique identifier for the plugin. * * @return a string representation of the plugin identifier. */ @Memoized public String id() { return String.format("/%s/%s/%s/%s", author(), type(), name(), version()); } /** * Factory method for creating a {@link PluginDefinition} from the {@link TsunamiPlugin} class. * * @param pluginClazz the {@link Class} of the Tsunami plugin * @return a {@link PluginDefinition} built from all the definition details about the plugin. */ public static PluginDefinition forPlugin(Class pluginClazz) { Optional pluginInfo = Optional.ofNullable(pluginClazz.getAnnotation(PluginInfo.class)); Optional targetServiceName = Optional.ofNullable(pluginClazz.getAnnotation(ForServiceName.class)); Optional targetSoftware = Optional.ofNullable(pluginClazz.getAnnotation(ForSoftware.class)); boolean isForWebService = pluginClazz.isAnnotationPresent(ForWebService.class); Optional targetOperatingSystemClass = Optional.ofNullable(pluginClazz.getAnnotation(ForOperatingSystemClass.class)); boolean requiresCallbackServer = pluginClazz.isAnnotationPresent(RequiresCallbackServer.class); checkState( pluginInfo.isPresent(), "A @PluginInfo annotation is required when creating a PluginDefinition for plugin: %s", pluginClazz); return new AutoValue_PluginDefinition( pluginInfo.get().type(), pluginInfo.get().name(), pluginInfo.get().author(), pluginInfo.get().version(), targetServiceName, targetSoftware, isForWebService, targetOperatingSystemClass, requiresCallbackServer); } /** * Factory method for creating a {@link PluginDefinition} for {@link RemoteVulnDetector} * implementations using the {@link PluginInfo} class. * * @param remotePluginInfo the {@link PluginInfo} of the remote Tsunami plugin * @return a {@link PluginDefinition} built from the plugin info. */ public static PluginDefinition forRemotePlugin(PluginInfo remotePluginInfo) { checkNotNull(remotePluginInfo); return new AutoValue_PluginDefinition( remotePluginInfo.type(), remotePluginInfo.name(), remotePluginInfo.author(), remotePluginInfo.version(), Optional.empty(), Optional.empty(), false, Optional.empty(), false); } /** * Factory method for creating a {@link PluginDefinition} for dynamic plugins. A dynamic plugin is * a plugin that is created at runtime and for which we cannot rely on the PluginInfo annotation. * *

Note that for dynamic plugins, we drop the notion of version and forces it to be 1.0. * * @param pluginName the name of the plugin * @param pluginAuthor the author of the plugin * @param isForWebService whether the plugin is for web service * @param requiresCallbackServer whether the plugin requires a callback server * @return a {@link PluginDefinition} built from the plugin info. */ public static PluginDefinition forDynamicPlugin( PluginType pluginType, String pluginName, String pluginAuthor, boolean isForWebService, boolean requiresCallbackServer) { return new AutoValue_PluginDefinition( pluginType, pluginName, pluginAuthor, "1.0", Optional.empty(), Optional.empty(), isForWebService, Optional.empty(), requiresCallbackServer); } } ================================================ FILE: plugin/src/main/java/com/google/tsunami/plugin/PluginExecutionException.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.plugin; import com.google.tsunami.common.ErrorCode; import com.google.tsunami.common.TsunamiException; /** Exception when executing a Tsunami plugin. */ public final class PluginExecutionException extends TsunamiException { public PluginExecutionException(String message) { super(ErrorCode.PLUGIN_EXECUTION_ERROR, message); } public PluginExecutionException(String message, Throwable cause) { super(ErrorCode.PLUGIN_EXECUTION_ERROR, message, cause); } } ================================================ FILE: plugin/src/main/java/com/google/tsunami/plugin/PluginExecutionModule.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.plugin; import com.google.inject.AbstractModule; import com.google.tsunami.common.concurrent.ScheduledThreadPoolModule; /** Installs dependencies used for plugin executions. */ public final class PluginExecutionModule extends AbstractModule { @Override protected void configure() { install(new PluginExecutorModule()); install( new ScheduledThreadPoolModule.Builder() .setName("PluginExecution") .setSize(16) .setDaemon(true) .setPriority(Thread.NORM_PRIORITY) .setAnnotation(PluginExecutionThreadPool.class) .build()); } } ================================================ FILE: plugin/src/main/java/com/google/tsunami/plugin/PluginExecutionResult.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.plugin; import com.google.auto.value.AutoValue; import com.google.common.base.Stopwatch; import com.google.tsunami.plugin.PluginExecutor.PluginExecutorConfig; import java.util.Optional; /** The result of executing the Tsunami plugin core logic by the {@link PluginExecutor}. */ @AutoValue public abstract class PluginExecutionResult { /** All possible execution status of a Tsunami plugin. */ public enum ExecutionStatus { SUCCEEDED, FAILED, TIMED_OUT } public boolean isSucceeded() { return executionStatus().equals(ExecutionStatus.SUCCEEDED); } public abstract ExecutionStatus executionStatus(); public abstract Optional resultData(); public abstract Stopwatch executionStopwatch(); public abstract Optional exception(); public abstract PluginExecutorConfig executorConfig(); public static Builder builder() { return new AutoValue_PluginExecutionResult.Builder<>(); } /** Builder for {@link PluginExecutionResult}. */ @AutoValue.Builder public abstract static class Builder { public abstract Builder setExecutionStatus(ExecutionStatus executionStatus); public abstract Builder setResultData(T resultData); public abstract Builder setExecutionStopwatch(Stopwatch executionStopwatch); public abstract Builder setException(PluginExecutionException exception); public abstract Builder setExecutorConfig(PluginExecutorConfig executorConfig); public abstract PluginExecutionResult build(); } } ================================================ FILE: plugin/src/main/java/com/google/tsunami/plugin/PluginExecutionThreadPool.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.plugin; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import javax.inject.Qualifier; /** Annotates the thread pool to use for executing Tsunami plugins. */ @Qualifier @Retention(RetentionPolicy.RUNTIME) public @interface PluginExecutionThreadPool {} ================================================ FILE: plugin/src/main/java/com/google/tsunami/plugin/PluginExecutor.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.plugin; import com.google.auto.value.AutoValue; import com.google.common.util.concurrent.ListenableFuture; import com.google.tsunami.plugin.PluginManager.PluginMatchingResult; import java.util.concurrent.Callable; /** The interface of the component to execute Tsunami plugins. */ public interface PluginExecutor { /** * Executes a plugin's core business logic implemented in non-block manner. * * @param executorConfig The configuration of the execution, cannot be null. * @param type of the plugin execution result. * @return The future of the execution result. */ ListenableFuture> executeAsync( PluginExecutorConfig executorConfig); /** Configures a plugin's core business logic to be executed by the {@link PluginExecutor}. */ @AutoValue abstract class PluginExecutorConfig { @SuppressWarnings("rawtypes") // AutoValue bug for not handling generic correctly in this case. public abstract PluginMatchingResult matchedPlugin(); public abstract Callable pluginExecutionLogic(); public static Builder builder() { return new AutoValue_PluginExecutor_PluginExecutorConfig.Builder<>(); } @AutoValue.Builder public abstract static class Builder { @SuppressWarnings("rawtypes") // AutoValue bug for not handling generic correctly. public abstract Builder setMatchedPlugin(PluginMatchingResult matchedPlugin); public abstract Builder setPluginExecutionLogic(Callable pluginExecutionLogic); public abstract PluginExecutorConfig build(); } } } ================================================ FILE: plugin/src/main/java/com/google/tsunami/plugin/PluginExecutorImpl.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.plugin; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import com.google.common.base.Stopwatch; import com.google.common.flogger.GoogleLogger; import com.google.common.util.concurrent.FluentFuture; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningScheduledExecutorService; import com.google.tsunami.plugin.PluginExecutionResult.ExecutionStatus; import java.time.Duration; import javax.inject.Inject; class PluginExecutorImpl implements PluginExecutor { private static final GoogleLogger logger = GoogleLogger.forEnclosingClass(); private final ListeningScheduledExecutorService pluginExecutionThreadPool; private final Stopwatch executionStopwatch; @Inject PluginExecutorImpl( @PluginExecutionThreadPool ListeningScheduledExecutorService pluginExecutionThreadPool) { this(pluginExecutionThreadPool, Stopwatch.createUnstarted()); } PluginExecutorImpl( ListeningScheduledExecutorService pluginExecutionThreadPool, Stopwatch executionStopwatch) { this.pluginExecutionThreadPool = checkNotNull(pluginExecutionThreadPool); this.executionStopwatch = checkNotNull(executionStopwatch); } @Override public ListenableFuture> executeAsync( PluginExecutorConfig executorConfig) { // Executes the core plugin logic within the thread pool. return FluentFuture.from( pluginExecutionThreadPool.submit( () -> { executionStopwatch.start(); return executorConfig.pluginExecutionLogic().call(); })) // Terminate plugin if it runs over 1 hour. .withTimeout(Duration.ofHours(1), pluginExecutionThreadPool) // If execution succeeded, build successful execution result. .transform(resultData -> buildSucceededResult(resultData, executorConfig), directExecutor()) // If execution failed, build failed execution result. .catching( Throwable.class, exception -> buildFailedResult(exception, executorConfig), directExecutor()); } private PluginExecutionResult buildSucceededResult( T resultData, PluginExecutorConfig executorConfig) { if (executionStopwatch.isRunning()) { executionStopwatch.stop(); } logger.atInfo().log( "%s plugin execution finished in %d (ms)", executorConfig.matchedPlugin().pluginDefinition().name(), executionStopwatch.elapsed().toMillis()); return PluginExecutionResult.builder() .setExecutionStatus(ExecutionStatus.SUCCEEDED) .setResultData(resultData) .setExecutionStopwatch(executionStopwatch) .setExecutorConfig(executorConfig) .build(); } private PluginExecutionResult buildFailedResult( Throwable t, PluginExecutorConfig executorConfig) { logger.atWarning().log( "Plugin '%s' failed: %s", executorConfig.matchedPlugin().pluginId(), t.getMessage()); if (executionStopwatch.isRunning()) { executionStopwatch.stop(); } return PluginExecutionResult.builder() .setExecutionStatus(ExecutionStatus.FAILED) .setExecutionStopwatch(executionStopwatch) .setException(wrapException(t, executorConfig)) .setExecutorConfig(executorConfig) .build(); } private static PluginExecutionException wrapException( Throwable t, PluginExecutorConfig executorConfig) { if (t instanceof PluginExecutionException) { return (PluginExecutionException) t; } else { return new PluginExecutionException( String.format( "Plugin execution error on '%s'.", executorConfig.matchedPlugin().pluginId()), t); } } } ================================================ FILE: plugin/src/main/java/com/google/tsunami/plugin/PluginExecutorModule.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.plugin; import com.google.inject.AbstractModule; /** Install bindings for {@link PluginExecutor}. */ public final class PluginExecutorModule extends AbstractModule { @Override protected void configure() { bind(PluginExecutor.class).to(PluginExecutorImpl.class); } } ================================================ FILE: plugin/src/main/java/com/google/tsunami/plugin/PluginLoadingModule.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.plugin; import static com.google.common.base.Preconditions.checkNotNull; import com.google.common.annotations.VisibleForTesting; import com.google.common.flogger.GoogleLogger; import com.google.inject.AbstractModule; import io.github.classgraph.AnnotationClassRef; import io.github.classgraph.ClassInfo; import io.github.classgraph.ClassInfoList; import io.github.classgraph.ScanResult; import java.lang.reflect.Constructor; /** * A Guice module that loads all {@link TsunamiPlugin TsunamiPlugins} at runtime. * *

This module relies on the {@link io.github.classgraph.ClassGraph} scan results to identify all * installed {@link TsunamiPlugin TsunamiPlugins} and bootstrap each {@link TsunamiPlugin plugin} * using the corresponding {@link PluginBootstrapModule} instantiated via reflection. */ public final class PluginLoadingModule extends AbstractModule { private static final GoogleLogger logger = GoogleLogger.forEnclosingClass(); private static final String TSUNAMI_PLUGIN_INTERFACE = "com.google.tsunami.plugin.TsunamiPlugin"; private static final String PLUGIN_INFO_ANNOTATION = "com.google.tsunami.plugin.annotations.PluginInfo"; private static final String BOOTSTRAP_MODULE_PARAM_NAME = "bootstrapModule"; private final boolean bootstrapModuleAlwaysAccessible; private final ScanResult classScanResult; public PluginLoadingModule(ScanResult classScanResult) { this(false, classScanResult); } @VisibleForTesting PluginLoadingModule(boolean bootstrapModuleAlwaysAccessible, ScanResult classScanResult) { this.bootstrapModuleAlwaysAccessible = bootstrapModuleAlwaysAccessible; this.classScanResult = checkNotNull(classScanResult); } @Override protected void configure() { ClassInfoList tsunamiPluginClasses = classScanResult .getClassesImplementing(TSUNAMI_PLUGIN_INTERFACE) .filter( classInfo -> !classInfo.isInterface() && !classInfo.implementsInterface( "com.google.tsunami.plugin.RemoteVulnDetector")); for (ClassInfo tsunamiPluginClass : tsunamiPluginClasses) { logger.atInfo().log("Found plugin class: %s", tsunamiPluginClass.getName()); // PluginInfo annotation is required for TsunamiPlugin. if (!tsunamiPluginClass.hasAnnotation(PLUGIN_INFO_ANNOTATION)) { throw new IllegalStateException( String.format( "Tsunami plugin '%s' must be annotated with PluginInfo", tsunamiPluginClass.getSimpleName())); } install(newPluginBootstrapModule(tsunamiPluginClass)); } } private PluginBootstrapModule newPluginBootstrapModule(ClassInfo tsunamiPluginClass) { // Retrieves the bootstrap module from the PluginInfo annotation. Object bootstrapModuleValue = tsunamiPluginClass .getAnnotationInfo(PLUGIN_INFO_ANNOTATION) .getParameterValues() .getValue(BOOTSTRAP_MODULE_PARAM_NAME); if (!(bootstrapModuleValue instanceof AnnotationClassRef)) { throw new AssertionError( String.format( "Invalid bootstrapModule parameter type for Tsunami plugin '%s'", tsunamiPluginClass.getSimpleName())); } ClassInfo bootstrapModuleClassInfo = ((AnnotationClassRef) bootstrapModuleValue).getClassInfo(); if (bootstrapModuleClassInfo == null) { throw new AssertionError( String.format( "bootstrapModule class for plugin '%s' not found in classpath", tsunamiPluginClass.getSimpleName())); } // Instantiate the bootstrap module via reflection. try { Constructor pluginBootstrapModuleConstructor = bootstrapModuleClassInfo.loadClass(PluginBootstrapModule.class).getDeclaredConstructor(); if (bootstrapModuleAlwaysAccessible) { pluginBootstrapModuleConstructor.setAccessible(true); } return pluginBootstrapModuleConstructor.newInstance(); } catch (ReflectiveOperationException e) { throw new AssertionError( String.format( "PluginBootstrapModule '%s' for plugin '%s' must be publicly constructable via a" + " no-argument constructor", bootstrapModuleClassInfo.getSimpleName(), tsunamiPluginClass.getSimpleName()), e); } } } ================================================ FILE: plugin/src/main/java/com/google/tsunami/plugin/PluginManager.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.plugin; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.collect.ImmutableSet.toImmutableSet; import static com.google.tsunami.common.data.NetworkServiceUtils.isWebService; import static java.util.Arrays.stream; import com.google.auto.value.AutoValue; import com.google.common.base.Ascii; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Streams; import com.google.tsunami.plugin.annotations.ForOperatingSystemClass; import com.google.tsunami.proto.MatchedPlugin; import com.google.tsunami.proto.NetworkService; import com.google.tsunami.proto.ReconnaissanceReport; import com.google.tsunami.proto.TargetInfo; import com.google.tsunami.proto.TargetOperatingSystemClass; import java.util.List; import java.util.Map; import java.util.Optional; import javax.inject.Inject; import javax.inject.Provider; /** * Plugin manager manages all the registered plugins and provides map like interfaces for retrieving * plugins from the registry. */ public class PluginManager { private final Map> tsunamiPlugins; private final TcsClient tcsClient; private final ImmutableSet detectorsInclude; private final ImmutableSet detectorsExclude; @Inject PluginManager( Map> tsunamiPlugins, TcsClient tcsClient, PluginManagerCliOptions pluginManagerCliOptions) { this.tsunamiPlugins = tsunamiPlugins; this.tcsClient = checkNotNull(tcsClient); detectorsInclude = getDetectorNames(pluginManagerCliOptions.detectorsInclude); detectorsExclude = getDetectorNames(pluginManagerCliOptions.detectorsExclude); } private static ImmutableSet getDetectorNames(String detectorNames) { if (detectorNames == null) { return ImmutableSet.of(); } else { return stream(detectorNames.split(",")).map(String::trim).collect(toImmutableSet()); } } /** * Retrieves all {@link PortScanner} plugins. * * @return a list of all the installed {@link PortScanner} plugins. */ public ImmutableList> getPortScanners() { return tsunamiPlugins.entrySet().stream() .filter(entry -> entry.getKey().type().equals(PluginType.PORT_SCAN)) .map( entry -> PluginMatchingResult.builder() .setPluginDefinition(entry.getKey()) .setTsunamiPlugin((PortScanner) entry.getValue().get()) .build()) .collect(toImmutableList()); } /** * Retrieves the first {@link PortScanner} plugin if present. * * @return the first installed {@link PortScanner} plugin. */ public Optional> getPortScanner() { ImmutableList> allPortScanners = getPortScanners(); return allPortScanners.isEmpty() ? Optional.empty() : Optional.of(allPortScanners.get(0)); } /** * Retrieves a {@link ServiceFingerprinter} plugin for the given {@link NetworkService}. * * @param networkService the target {@link NetworkService} to be fingerprinted. * @return the matched {@link ServiceFingerprinter} plugin for the given network service. */ public Optional> getServiceFingerprinter( NetworkService networkService) { return tsunamiPlugins.entrySet().stream() .filter(entry -> entry.getKey().type().equals(PluginType.SERVICE_FINGERPRINT)) .filter(entry -> hasMatchingServiceName(networkService, entry.getKey())) .map( entry -> PluginMatchingResult.builder() .setPluginDefinition(entry.getKey()) .setTsunamiPlugin((ServiceFingerprinter) entry.getValue().get()) .addMatchedService(networkService) .build()) .findFirst(); } public ImmutableList> getVulnDetectors( ReconnaissanceReport reconnaissanceReport) { return tsunamiPlugins.entrySet().stream() .filter(entry -> isVulnDetector(entry.getKey())) .filter(entry -> matchCurrentCallbackServerSetup(entry.getKey())) .filter(entry -> filterPluginByCliOptions(entry.getKey())) .map(entry -> matchAllVulnDetectors(entry.getKey(), entry.getValue(), reconnaissanceReport)) .flatMap(Streams::stream) .collect(toImmutableList()); } public ImmutableList getAllVulnDetectors() { return tsunamiPlugins.entrySet().stream() .filter(entry -> isVulnDetector(entry.getKey())) .map( entry -> { if (entry.getKey().type().equals(PluginType.VULN_DETECTION)) { return (VulnDetector) entry.getValue().get(); } return (RemoteVulnDetector) entry.getValue().get(); }) .collect(toImmutableList()); } private static boolean isPluginListed( PluginDefinition pluginDefinition, ImmutableSet pluginNames, boolean defaultValue) { if (pluginNames.isEmpty()) { return defaultValue; } return pluginNames.contains(pluginDefinition.name()); } private boolean filterPluginByCliOptions(PluginDefinition pluginDefinition) { return isPluginListed(pluginDefinition, detectorsInclude, true) && !isPluginListed(pluginDefinition, detectorsExclude, false); } private static boolean isVulnDetector(PluginDefinition pluginDefinition) { return pluginDefinition.type().equals(PluginType.VULN_DETECTION) || pluginDefinition.type().equals(PluginType.REMOTE_VULN_DETECTION); } private boolean matchCurrentCallbackServerSetup(PluginDefinition pluginDefinition) { if (tcsClient.isCallbackServerEnabled()) { return true; } return !pluginDefinition.requiresCallbackServer(); } private static Optional> matchAllVulnDetectors( PluginDefinition pluginDefinition, Provider vulnDetectorProvider, ReconnaissanceReport reconnaissanceReport) { if (pluginDefinition.type().equals(PluginType.REMOTE_VULN_DETECTION)) { return matchRemoteVulnDetectors(pluginDefinition, vulnDetectorProvider, reconnaissanceReport); } return matchVulnDetectors(pluginDefinition, vulnDetectorProvider, reconnaissanceReport); } private static Optional> matchVulnDetectors( PluginDefinition pluginDefinition, Provider vulnDetectorProvider, ReconnaissanceReport reconnaissanceReport) { List matchedNetworkServices; var allNetworkServices = reconnaissanceReport.getNetworkServicesList(); if (pluginDefinition.targetOperatingSystemClass().isPresent()) { allNetworkServices = allNetworkServices.stream() .filter( networkService -> hasMatchingOperatingSystem( reconnaissanceReport.getTargetInfo(), pluginDefinition)) .collect(toImmutableList()); } if (!pluginDefinition.targetServiceName().isPresent() && !pluginDefinition.targetSoftware().isPresent() && !pluginDefinition.isForWebService()) { // No filtering annotation applied, just match all network services from reconnaissance. matchedNetworkServices = allNetworkServices; } else { // At least one filtering annotation applied, check services to see if any one matches. matchedNetworkServices = allNetworkServices.stream() .filter( networkService -> hasMatchingServiceName(networkService, pluginDefinition) || hasMatchingSoftware(networkService, pluginDefinition)) .collect(toImmutableList()); } return matchedNetworkServices.isEmpty() ? Optional.empty() : Optional.of( PluginMatchingResult.builder() .setPluginDefinition(pluginDefinition) .setTsunamiPlugin((VulnDetector) vulnDetectorProvider.get()) .addAllMatchedServices(matchedNetworkServices) .build()); } private static Optional> matchRemoteVulnDetectors( PluginDefinition pluginDefinition, Provider tsunamiPlugin, ReconnaissanceReport reconnaissanceReport) { var remoteVulnDetector = (RemoteVulnDetector) tsunamiPlugin.get(); var builder = PluginMatchingResult.builder() .setTsunamiPlugin(remoteVulnDetector) // PluginDefinition class for the RemoteVulnDetector. .setPluginDefinition(pluginDefinition) .addAllMatchedServices(reconnaissanceReport.getNetworkServicesList()); for (com.google.tsunami.proto.PluginDefinition remotePluginDefinition : remoteVulnDetector.getAllPlugins()) { var matchedPluginBuilder = MatchedPlugin.newBuilder(); var allNetworkServices = reconnaissanceReport.getNetworkServicesList(); if (remotePluginDefinition.hasTargetOperatingSystemClass()) { // Prefiltering based on the Operating System, so the other potential filters are applied // like if we were in an AND condition. allNetworkServices = allNetworkServices.stream() .filter( networkService -> hasMatchingOperatingSystem( reconnaissanceReport.getTargetInfo(), remotePluginDefinition)) .collect(toImmutableList()); } if (!remotePluginDefinition.hasTargetServiceName() && !remotePluginDefinition.hasTargetSoftware() && !remotePluginDefinition.getForWebService()) { matchedPluginBuilder.setPlugin(remotePluginDefinition).addAllServices(allNetworkServices); } else { matchedPluginBuilder .setPlugin(remotePluginDefinition) .addAllServices( allNetworkServices.stream() .filter( networkService -> hasMatchingServiceName(networkService, remotePluginDefinition) || hasMatchingSoftware(networkService, remotePluginDefinition)) .collect(toImmutableList())); } remoteVulnDetector.addMatchedPluginToDetect(matchedPluginBuilder.build()); } return Optional.of(builder.build()); } private static boolean hasMatchingOperatingSystem( TargetInfo targetInfo, PluginDefinition pluginDefinition) { return hasMatchingOperatingSystem( targetInfo, getTargetOperatingSystemClass(pluginDefinition.targetOperatingSystemClass().get())); } private static boolean hasMatchingOperatingSystem( TargetInfo targetInfo, com.google.tsunami.proto.PluginDefinition pluginDefinition) { return hasMatchingOperatingSystem(targetInfo, pluginDefinition.getTargetOperatingSystemClass()); } // Determines if the target info has a matching operating system to the plugin definition. // If the target info has no info about the operating system, it will return false. private static boolean hasMatchingOperatingSystem( TargetInfo targetInfo, TargetOperatingSystemClass pluginOs) { for (var osGuess : targetInfo.getOperatingSystemClassesList()) { var osGuessAccuracy = osGuess.getAccuracy(); var minAccuracyWanted = pluginOs.getMinAccuracy(); if (minAccuracyWanted != 0 && minAccuracyWanted > osGuessAccuracy) { continue; } if (pluginOs.getVendorList().contains(osGuess.getVendor())) { return true; } if (pluginOs.getOsFamilyList().contains(osGuess.getOsFamily())) { return true; } } // None of the OS guesses matched. return false; } private static boolean hasMatchingServiceName( NetworkService networkService, PluginDefinition pluginDefinition) { String serviceName = networkService.getServiceName(); boolean hasServiceNameMatch = pluginDefinition.targetServiceName().isPresent() && (serviceName.isEmpty() || stream(pluginDefinition.targetServiceName().get().value()) .anyMatch( targetServiceName -> Ascii.equalsIgnoreCase(targetServiceName, serviceName))); boolean hasWebServiceMatch = pluginDefinition.isForWebService() && isWebService(networkService); return hasServiceNameMatch || hasWebServiceMatch; } private static boolean hasMatchingServiceName( NetworkService networkService, com.google.tsunami.proto.PluginDefinition pluginDefinition) { String serviceName = networkService.getServiceName(); boolean hasServiceNameMatch = pluginDefinition.hasTargetServiceName() && (serviceName.isEmpty() || pluginDefinition.getTargetServiceName().getValueList().stream() .anyMatch( targetServiceName -> Ascii.equalsIgnoreCase(targetServiceName, serviceName))); boolean hasWebServiceMatch = pluginDefinition.getForWebService() && isWebService(networkService); return hasServiceNameMatch || hasWebServiceMatch; } private static boolean hasMatchingSoftware( NetworkService networkService, PluginDefinition pluginDefinition) { String softwareName = networkService.getSoftware().getName(); return pluginDefinition.targetSoftware().isPresent() && (softwareName.isEmpty() || Ascii.equalsIgnoreCase( pluginDefinition.targetSoftware().get().name(), softwareName)); } private static boolean hasMatchingSoftware( NetworkService networkService, com.google.tsunami.proto.PluginDefinition pluginDefinition) { String softwareName = networkService.getSoftware().getName(); return pluginDefinition.hasTargetSoftware() && (softwareName.isEmpty() || Ascii.equalsIgnoreCase( pluginDefinition.getTargetSoftware().getName(), softwareName)); } /** Matched {@link TsunamiPlugin}s based on certain criteria. */ @AutoValue public abstract static class PluginMatchingResult { public abstract PluginDefinition pluginDefinition(); public abstract T tsunamiPlugin(); public abstract ImmutableList matchedServices(); public String pluginId() { return pluginDefinition().id(); } public static Builder builder() { return new AutoValue_PluginManager_PluginMatchingResult.Builder(); } /** Builder for {@link PluginMatchingResult}. */ @SuppressWarnings("CanIgnoreReturnValueSuggester") @AutoValue.Builder public abstract static class Builder { public abstract Builder setPluginDefinition(PluginDefinition value); public abstract Builder setTsunamiPlugin(T value); abstract ImmutableList.Builder matchedServicesBuilder(); public Builder addMatchedService(NetworkService networkService) { matchedServicesBuilder().add(networkService); return this; } public Builder addAllMatchedServices(Iterable networkServices) { matchedServicesBuilder().addAll(networkServices); return this; } public abstract PluginMatchingResult build(); } } private static TargetOperatingSystemClass getTargetOperatingSystemClass( ForOperatingSystemClass target) { var builder = TargetOperatingSystemClass.newBuilder().setMinAccuracy(target.minAccuracy()); for (String vendor : target.vendor()) { builder.addVendor(vendor); } for (String osfamily : target.osfamily()) { builder.addOsFamily(osfamily); } return builder.build(); } } ================================================ FILE: plugin/src/main/java/com/google/tsunami/plugin/PluginManagerCliOptions.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.plugin; import com.beust.jcommander.Parameter; import com.beust.jcommander.Parameters; import com.google.tsunami.common.cli.CliOption; /** * Command line arguments for the PluginManager. * *

Which detectors to include/exclude? Matching is executed against the name of the detector * (e.g. RsyncRceDetector). * *

To execute one single detector, override `detectors-include`, e.g.: * `--detectors-include=RsyncRceDetector`. * *

To disable a single detector, override `detectors-exclude`, e.g. * `--detectors-exclude=RsyncRceDetector`. */ @Parameters(separators = "=") public final class PluginManagerCliOptions implements CliOption { @Parameter( names = "--detectors-include", description = "Comma separated list of detector names to include in the scan. By default, all detectors" + " are included.") public String detectorsInclude; @Parameter( names = "--detectors-exclude", description = "Comma separated list of detector names to exclude from the scan. By default no detectors" + " are skipped.") public String detectorsExclude; // Validations are done in {@link PayloadGeneratorModule}. @Override public void validate() {} } ================================================ FILE: plugin/src/main/java/com/google/tsunami/plugin/PluginServiceClient.java ================================================ /* * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.plugin; import static com.google.common.base.Preconditions.checkNotNull; import com.google.common.util.concurrent.ListenableFuture; import com.google.tsunami.proto.ListPluginsRequest; import com.google.tsunami.proto.ListPluginsResponse; import com.google.tsunami.proto.PluginServiceGrpc; import com.google.tsunami.proto.PluginServiceGrpc.PluginServiceFutureStub; import com.google.tsunami.proto.RunCompactRequest; import com.google.tsunami.proto.RunRequest; import com.google.tsunami.proto.RunResponse; import io.grpc.Channel; import io.grpc.Deadline; import io.grpc.health.v1.HealthCheckRequest; import io.grpc.health.v1.HealthCheckResponse; import io.grpc.health.v1.HealthGrpc; import io.grpc.health.v1.HealthGrpc.HealthFutureStub; /** * Client side gRPC handler for the PluginService RPC protocol. Main handler for all gRPC calls to * the language-specific servers. */ public final class PluginServiceClient { private final HealthFutureStub healthService; private final PluginServiceFutureStub pluginService; PluginServiceClient(Channel channel) { this.healthService = HealthGrpc.newFutureStub(checkNotNull(channel)); this.pluginService = PluginServiceGrpc.newFutureStub(checkNotNull(channel)); } /** * Sends a run request to the gRPC language server with a specified deadline. * * @param request The main request containing plugins to run. * @param deadline The timeout of the service. * @return The future of the run response. */ public ListenableFuture runWithDeadline(RunRequest request, Deadline deadline) { return pluginService.withDeadline(deadline).run(request); } /** * Sends a runCompact request to the gRPC language server with a specified deadline. * * @param request The main request containing plugins to run. * @param deadline The timeout of the service. * @return The future of the run response. */ public ListenableFuture runCompactWithDeadline( RunCompactRequest request, Deadline deadline) { return pluginService.withDeadline(deadline).runCompact(request); } /** * Sends a list plugins request to the gRPC language server with a specified deadline. * * @param request The main request to notify the language server to send their plugins. * @param deadline The timeout of the service. * @return The future of the run response. */ public ListenableFuture listPluginsWithDeadline( ListPluginsRequest request, Deadline deadline) { return pluginService.withDeadline(deadline).listPlugins(request); } /** * Sends a health check request to retrieve the status of the language server. * * @param request The health check request to send to the language server. * @param deadline The maximum time to keep this call alive. * @return The language server's status via {@link HealthCheckResponse}. */ public ListenableFuture checkHealthWithDeadline( HealthCheckRequest request, Deadline deadline) { return healthService.withDeadline(deadline).check(request); } } ================================================ FILE: plugin/src/main/java/com/google/tsunami/plugin/PluginType.java ================================================ /* * Copyright 2019 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.plugin; /** The types of a Tsunami plugin. */ public enum PluginType { /** A plugin that identifies the open ports of the scanning target. */ PORT_SCAN, /** * A plugin that performs service specific fingerprints and identifies running software and * versions. */ SERVICE_FINGERPRINT, /** A plugin that detects certain vulnerabilities on an exposed network service. */ VULN_DETECTION, /** A plugin that contains vulnerability detectors from language servers. */ REMOTE_VULN_DETECTION } ================================================ FILE: plugin/src/main/java/com/google/tsunami/plugin/PortScanner.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.plugin; import com.google.tsunami.proto.PortScanningReport; import com.google.tsunami.proto.ScanTarget; /** * A {@link TsunamiPlugin} that performs the port scanning tasks. * *

A port scanner in general should perform network probing to identify network services (active * ports) running on a target and/or gather any interesting information about the scanning target * like its architecture, operating systems, etc. */ public interface PortScanner extends TsunamiPlugin { /** * Performs the port scan on the given {@code scanTarget}. * * @param scanTarget the target to be scanned. * @return a {@link PortScanningReport} that captures the full port scanning results. */ PortScanningReport scan(ScanTarget scanTarget); } ================================================ FILE: plugin/src/main/java/com/google/tsunami/plugin/RemoteVulnDetector.java ================================================ /* * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.plugin; import com.google.common.collect.ImmutableList; import com.google.tsunami.proto.MatchedPlugin; import com.google.tsunami.proto.PluginDefinition; /** * A special {@link VulnDetector} to execute vulnerability detector plugins from their specified * language server. */ public interface RemoteVulnDetector extends VulnDetector { /** * Retrieve all plugins from the language server through an RPC call. * * @return List of all plugin definitions. If the language server throws an error, an empty list * will be returned. */ ImmutableList getAllPlugins(); /** * Add a {@link MatchedPlugin} to allow this {@link RemoteVulnDetector} to run detection for this * plugin through the language server. * * @param pluginToRun The plugin to allow this {@link RemoteVulnDetector} to run. */ void addMatchedPluginToDetect(MatchedPlugin pluginToRun); } ================================================ FILE: plugin/src/main/java/com/google/tsunami/plugin/RemoteVulnDetectorImpl.java ================================================ /* * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.plugin; import static com.google.common.base.Preconditions.checkNotNull; import static java.util.concurrent.TimeUnit.SECONDS; import com.google.api.client.util.BackOff; import com.google.api.client.util.ExponentialBackOff; import com.google.common.collect.ImmutableList; import com.google.common.collect.Sets; import com.google.common.flogger.GoogleLogger; import com.google.common.util.concurrent.Uninterruptibles; import com.google.tsunami.common.server.CompactRunRequestHelper; import com.google.tsunami.proto.DetectionReportList; import com.google.tsunami.proto.ListPluginsRequest; import com.google.tsunami.proto.MatchedPlugin; import com.google.tsunami.proto.NetworkService; import com.google.tsunami.proto.PluginDefinition; import com.google.tsunami.proto.RunRequest; import com.google.tsunami.proto.RunResponse; import com.google.tsunami.proto.TargetInfo; import com.google.tsunami.proto.Vulnerability; import io.grpc.Channel; import io.grpc.Deadline; import io.grpc.health.v1.HealthCheckRequest; import io.grpc.health.v1.HealthCheckResponse; import java.io.IOException; import java.time.Duration; import java.util.Set; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; /** Facilitates communication with remote detectors. */ public final class RemoteVulnDetectorImpl implements RemoteVulnDetector { private static final GoogleLogger logger = GoogleLogger.forEnclosingClass(); // Default duration deadline for the detect() RPC call // Remote detectors, especially ones using the callback server, require additional buffer to send // requests and responses. private static final Duration DEFAULT_DEADLINE_DETECT = Duration.ofSeconds(150); // For all other operations, including health check: private static final Duration DEFAULT_DEADLINE = Duration.ofSeconds(10); private final PluginServiceClient service; private final Set pluginsToRun; private final ExponentialBackOff backoff; private final int maxAttempts; private final Duration detectDeadline; private boolean wantCompactRunRequest = false; RemoteVulnDetectorImpl( Channel channel, ExponentialBackOff backoff, int maxAttempts, Duration detectDeadline) { this.service = new PluginServiceClient(checkNotNull(channel)); this.pluginsToRun = Sets.newHashSet(); this.backoff = backoff; this.maxAttempts = maxAttempts; this.detectDeadline = detectDeadline != null ? detectDeadline : DEFAULT_DEADLINE_DETECT; } @Override public DetectionReportList detect( TargetInfo target, ImmutableList matchedServices) { try { if (checkHealthWithBackoffs()) { var runRequest = RunRequest.newBuilder().setTarget(target).addAllPlugins(pluginsToRun).build(); logger.atInfo().log("Detecting with language server plugins..."); RunResponse runResponse; if (this.wantCompactRunRequest) { var runCompactRequest = CompactRunRequestHelper.compress(runRequest); runResponse = service .runCompactWithDeadline( runCompactRequest, Deadline.after(detectDeadline.toSeconds(), SECONDS)) .get(); } else { runResponse = service .runWithDeadline(runRequest, Deadline.after(detectDeadline.toSeconds(), SECONDS)) .get(); } return runResponse.getReports(); } } catch (InterruptedException | ExecutionException e) { throw new LanguageServerException("Failed to get response from language server.", e); } return DetectionReportList.getDefaultInstance(); } @Override public ImmutableList getAdvisories() { // TODO: b/422968545 - The remote detectors also need to support getAdvisories(). return ImmutableList.of(); } @Override public ImmutableList getAllPlugins() { try { if (checkHealthWithBackoffs()) { logger.atInfo().log("Getting language server plugins..."); var listPluginsResponse = service .listPluginsWithDeadline( ListPluginsRequest.getDefaultInstance(), Deadline.after(DEFAULT_DEADLINE.toSeconds(), SECONDS)) .get(); // Note: each plugin service client has a dedicated RemoteVulnDetectorImpl instance, // so we can safely set this flag here. this.wantCompactRunRequest = listPluginsResponse.getWantCompactRunRequest(); return ImmutableList.copyOf(listPluginsResponse.getPluginsList()); } else { return ImmutableList.of(); } } catch (InterruptedException | ExecutionException e) { throw new LanguageServerException("Failed to get response from language server.", e); } } private boolean checkHealthWithBackoffs() { // After starting the language server, this is our first attempt to establish a connection // between the Java and the language server. // Sometimes the language server may need longer time to ramp up its health service, so we need // to implement exponential retries to manage those circumstances. backoff.reset(); int attempt = 0; while (attempt < maxAttempts) { try { var healthy = service .checkHealthWithDeadline( HealthCheckRequest.getDefaultInstance(), Deadline.after(DEFAULT_DEADLINE.toSeconds(), SECONDS)) .get() .getStatus() .equals(HealthCheckResponse.ServingStatus.SERVING); if (!healthy) { logger.atWarning().log("Language server is not serving."); } return healthy; } catch (InterruptedException | ExecutionException e) { attempt++; try { long backOffMillis = backoff.nextBackOffMillis(); if (backOffMillis != BackOff.STOP) { Uninterruptibles.sleepUninterruptibly(backOffMillis, TimeUnit.MILLISECONDS); } } catch (IOException ioe) { // ignore logger.atWarning().log("Failed to sleep for %s", ioe.getCause().getMessage()); } if (attempt == maxAttempts) { throw new LanguageServerException("Language service is not registered.", e.getCause()); } } } return false; } @Override public void addMatchedPluginToDetect(MatchedPlugin plugin) { this.pluginsToRun.add(plugin); } } ================================================ FILE: plugin/src/main/java/com/google/tsunami/plugin/RemoteVulnDetectorLoadingModule.java ================================================ /* * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.plugin; import static com.google.common.base.Preconditions.checkNotNull; import com.google.api.client.util.ExponentialBackOff; import com.google.auto.value.AutoAnnotation; import com.google.auto.value.AutoBuilder; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.inject.AbstractModule; import com.google.inject.multibindings.MapBinder; import com.google.tsunami.common.server.LanguageServerCommand; import com.google.tsunami.plugin.annotations.PluginInfo; import io.grpc.Channel; import io.grpc.netty.NegotiationType; import io.grpc.netty.NettyChannelBuilder; import java.time.Duration; /** A Guice module that loads all {@link RemoteVulnDetector RemoteVulnDetectors} at runtime. */ public final class RemoteVulnDetectorLoadingModule extends AbstractModule { private static final int MAX_MESSAGE_SIZE = 10 * 1000 * 1000; // Max incoming gRPC message size 10MB. private static final int INITIAL_WAIT_TIME_MS = 200; private static final int MAX_WAIT_TIME_MS = 30000; private static final int WAIT_TIME_MULTIPLIER = 5; private static final int MAX_ATTEMPTS = 5; // Exponential delay attempts (>125 seconds before taking randomization factor into account): // ~200ms // ~1000ms // ~5000ms // ~25000ms // ~1250000ms private static final ExponentialBackOff BACKOFF = new ExponentialBackOff.Builder() .setInitialIntervalMillis(INITIAL_WAIT_TIME_MS) .setRandomizationFactor(0.1) .setMultiplier(WAIT_TIME_MULTIPLIER) .setMaxElapsedTimeMillis(MAX_WAIT_TIME_MS) .build(); private final ImmutableList availableServerPorts; public RemoteVulnDetectorLoadingModule(ImmutableList serverPorts) { this.availableServerPorts = checkNotNull(serverPorts); } @Override protected void configure() { MapBinder tsunamiPluginBinder = MapBinder.newMapBinder(binder(), PluginDefinition.class, TsunamiPlugin.class); availableServerPorts.forEach( command -> { var channel = getLanguageServerChannel(command); var deadline = command.deadlineRunSeconds() > 0 ? Duration.ofSeconds(command.deadlineRunSeconds()) : null; tsunamiPluginBinder .addBinding(getRemoteVulnDetectorPluginDefinition(channel.hashCode())) .toInstance(new RemoteVulnDetectorImpl(channel, BACKOFF, MAX_ATTEMPTS, deadline)); }); } private Channel getLanguageServerChannel(LanguageServerCommand command) { if (Strings.isNullOrEmpty(command.serverCommand())) { return NettyChannelBuilder.forTarget( String.format("%s:%s", command.serverAddress(), command.port())) .negotiationType(NegotiationType.PLAINTEXT) .maxInboundMessageSize(MAX_MESSAGE_SIZE) .build(); } else { // TODO(b/289462738): Support IPv6 loopback (::1) interface return NettyChannelBuilder.forTarget("127.0.0.1:" + command.port()) .negotiationType(NegotiationType.PLAINTEXT) .maxInboundMessageSize(MAX_MESSAGE_SIZE) .build(); } } // TODO(b/239095108): Change channelIds to something more meaningful to identify // RemoteVulnDetectors. @VisibleForTesting static PluginDefinition getRemoteVulnDetectorPluginDefinition(int channelId) { return PluginDefinition.forRemotePlugin( pluginInfoBuilder() .type(PluginType.REMOTE_VULN_DETECTION) .name("RemoteVulnDetector" + channelId) .description("Synthetic PluginInfo for RemoteVulnDetectors") .author("Tsunami") .version("0.0.1") .bootstrapModule(RemoteVulnDetectorBootstrapLoadingModule.class) .build()); } /** Builder to build a {@link PluginInfo} annotation at runtime for RemoteVulnDetectors. */ @AutoBuilder(callMethod = "pluginInfo") interface PluginInfoBuilder { PluginInfoBuilder type(PluginType type); PluginInfoBuilder name(String name); PluginInfoBuilder description(String description); PluginInfoBuilder author(String author); PluginInfoBuilder version(String version); PluginInfoBuilder bootstrapModule(Class bootstrapModule); PluginInfo build(); } // Used by {@link AutoBuilder} @SuppressWarnings("unused") @AutoAnnotation static PluginInfo pluginInfo( PluginType type, String name, String description, String author, String version, Class bootstrapModule) { return new AutoAnnotation_RemoteVulnDetectorLoadingModule_pluginInfo( type, name, description, author, version, bootstrapModule); } static PluginInfoBuilder pluginInfoBuilder() { return new AutoBuilder_RemoteVulnDetectorLoadingModule_PluginInfoBuilder(); } private static class RemoteVulnDetectorBootstrapLoadingModule extends PluginBootstrapModule { @Override protected void configurePlugin() {} } } ================================================ FILE: plugin/src/main/java/com/google/tsunami/plugin/ServiceFingerprinter.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.plugin; import com.google.tsunami.proto.FingerprintingReport; import com.google.tsunami.proto.NetworkService; import com.google.tsunami.proto.TargetInfo; /** * A {@link TsunamiPlugin} that performs the service fingerprinting tasks. * *

A fingerprinter usually performs service specific fingerprinting jobs to better understand an * exposed service. For example, a web fingerprinter would help the scanner identify which web * applications and their corresponding versions are exposed under port 80 or 443. * *

NOTE: ServiceFingerprinters must be annotated by the {@link * com.google.tsunami.plugin.annotations.ForServiceName} annotation. */ public interface ServiceFingerprinter extends TsunamiPlugin { /** * Performs the service fingerprinting job on the given {@code targetInfo} and {@code * networkService}. * * @param targetInfo information about the target to be scanned. * @param networkService information about the specific network service to be fingerprinted. * @return a {@link FingerprintingReport} that captures all the details about the targeted * service. */ FingerprintingReport fingerprint(TargetInfo targetInfo, NetworkService networkService); } ================================================ FILE: plugin/src/main/java/com/google/tsunami/plugin/TcsClient.java ================================================ /* * Copyright 2021 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.plugin; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.tsunami.common.net.UrlUtils.removeTrailingSlashes; import com.google.common.flogger.GoogleLogger; import com.google.common.net.HostAndPort; import com.google.common.net.InetAddresses; import com.google.common.net.InternetDomainName; import com.google.protobuf.util.JsonFormat; import com.google.tsunami.callbackserver.common.CbidGenerator; import com.google.tsunami.callbackserver.common.CbidProcessor; import com.google.tsunami.callbackserver.common.Sha3CbidGenerator; import com.google.tsunami.callbackserver.proto.PollingResult; import com.google.tsunami.common.net.http.HttpClient; import com.google.tsunami.common.net.http.HttpHeaders; import com.google.tsunami.common.net.http.HttpRequest; import com.google.tsunami.common.net.http.HttpResponse; import java.io.IOException; import java.util.Optional; /** TcsClient for generating oob payload and retriving result */ public final class TcsClient { private static final GoogleLogger logger = GoogleLogger.forEnclosingClass(); private static final JsonFormat.Parser jsonParser = JsonFormat.parser(); private final String callbackAddress; private final int callbackPort; private final String pollingBaseUrl; private final CbidGenerator cbidGenerator; private final HttpClient httpClient; public TcsClient( String callbackAddress, int callbackPort, String pollingBaseUrl, HttpClient httpClient) { this.callbackAddress = checkNotNull(callbackAddress); this.callbackPort = callbackPort; this.pollingBaseUrl = checkNotNull(pollingBaseUrl); this.cbidGenerator = new Sha3CbidGenerator(); this.httpClient = checkNotNull(httpClient); } /** * Checks whether the callback server is configured. Detectors should use this to determine if * they can use the callback server for detecting vulnerabilities * * @return whether the callback server is enabled */ public boolean isCallbackServerEnabled() { // only return false when all config fields are empty so that improper config (e.g., missing // certain fields) can be exposed. Note that {@link TcsClient} already checks that all the // class variables are not null. return !this.callbackAddress.isEmpty() && isValidPortNumber(this.callbackPort) && !this.pollingBaseUrl.isEmpty(); } public String getCallbackUri(String secretString) { String cbid = cbidGenerator.generate(secretString); if (!isValidPortNumber(callbackPort)) { throw new AssertionError("Invalid callbackPort number specified"); } HostAndPort hostAndPort = callbackPort == 80 ? HostAndPort.fromHost(callbackAddress) : HostAndPort.fromParts(callbackAddress, callbackPort); // check if the specified address is raw IP or domain if (InetAddresses.isInetAddress(callbackAddress)) { return CbidProcessor.addCbidToUrl(cbid, hostAndPort); } else if (InternetDomainName.isValid(callbackAddress)) { return CbidProcessor.addCbidToSubdomain(cbid, hostAndPort); } // Should never reach here throw new AssertionError("Unrecognized address format, should be IP address or valid domain"); } public String getCallbackAddress() { if (InetAddresses.isInetAddress(callbackAddress)) { return callbackAddress; } else if (InternetDomainName.isValid(callbackAddress)) { return callbackAddress; } // Should never reach here throw new AssertionError("Unrecognized address format, should be IP address or valid domain"); } public int getCallbackPort() { if (!isValidPortNumber(callbackPort)) { throw new AssertionError("Invalid callbackPort number specified"); } return callbackPort; } public boolean hasOobLog(String secretString) { // making a blocking call to get result Optional result = sendPollingRequest(secretString); if (result.isPresent()) { // In the future we may refactor hasOobLog() to return finer grained info about what kind // of oob is logged return result.get().getHasDnsInteraction() || result.get().getHasHttpInteraction(); } else { // we may choose to retry sendPollingRequest() if oob interactions do arrive late. return false; } } private Optional sendPollingRequest(String secretString) { HttpRequest request = HttpRequest.get( String.format("%s/?secret=%s", removeTrailingSlashes(pollingBaseUrl), secretString)) .setHeaders(HttpHeaders.builder().addHeader("Cache-Control", "no-cache").build()) .build(); try { HttpResponse response = httpClient.send(request); if (response.status().isSuccess()) { PollingResult.Builder result = PollingResult.newBuilder(); jsonParser.merge(response.bodyString().get(), result); return Optional.of(result.build()); } else { logger.atInfo().log("Callback server returned %s", response.status().code()); } } catch (IOException e) { logger.atWarning().withCause(e).log("Polling request failed"); } return Optional.empty(); } private static boolean isValidPortNumber(int port) { return port > 0 && port < 65536; } } ================================================ FILE: plugin/src/main/java/com/google/tsunami/plugin/TcsClientCliOptions.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.plugin; import com.beust.jcommander.Parameter; import com.beust.jcommander.Parameters; import com.google.tsunami.common.cli.CliOption; /** Command line arguments for the Tsunami callbackserver. */ @Parameters(separators = "=") public final class TcsClientCliOptions implements CliOption { @Parameter( names = "--callback-address", description = "Address (ip or domain) of TCS http service.") public String callbackAddress; @Parameter(names = "--callback-port", description = "Port of TCS http service.") public Integer callbackPort; @Parameter( names = "--callback-polling-uri", description = "Uri (ip/domain + port) of TCS polling service.") public String pollingUri; // Validations are done in {@link PayloadGeneratorModule}. @Override public void validate() {} } ================================================ FILE: plugin/src/main/java/com/google/tsunami/plugin/TcsConfigProperties.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.plugin; import com.google.tsunami.common.config.annotations.ConfigProperties; /** Configuration properties for Tsunami callbackserver. */ @ConfigProperties("plugin.callbackserver") public final class TcsConfigProperties { /** Address (ip or domain) of TCS http service. */ public String callbackAddress; /** Port of TCS http service. */ public Integer callbackPort; /** Uri (ip/domain + port) of TCS polling service. */ public String pollingUri; } ================================================ FILE: plugin/src/main/java/com/google/tsunami/plugin/TsunamiPlugin.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.plugin; /** * Simple plugin interface for Tsunami. * *

All concrete plugins for Tsunami must implement this interface. Otherwise it won't be * identified as a valid plugin for Tsunami. */ public interface TsunamiPlugin {} ================================================ FILE: plugin/src/main/java/com/google/tsunami/plugin/VulnDetector.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.plugin; import com.google.common.collect.ImmutableList; import com.google.tsunami.proto.DetectionReportList; import com.google.tsunami.proto.NetworkService; import com.google.tsunami.proto.TargetInfo; import com.google.tsunami.proto.Vulnerability; /** * A {@link TsunamiPlugin} that detects potential vulnerabilities on the target. * *

Usually a vulnerability detector takes the information about an exposed network service, * detects whether the service is vulnerable to a specific vulnerability, and reports the detection * results. */ public interface VulnDetector extends TsunamiPlugin { /** * Performs the detection task for specific vulnerabilities. * * @param targetInfo information about the scanning target itself * @param matchedServices a list of network services whose vulnerabilities could be detected by * this plugin * @return a {@link DetectionReportList} for all the vulnerabilities of the scanning target. */ DetectionReportList detect( TargetInfo targetInfo, ImmutableList matchedServices); /** * Provides access to the information on the vulnerabilities detected by this plugin. To avoid * confusion, information about a vulnerability is referred to as an advisory. * * @return the advisory details. */ abstract ImmutableList getAdvisories(); } ================================================ FILE: plugin/src/main/java/com/google/tsunami/plugin/annotations/ForOperatingSystemClass.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.plugin.annotations; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * An annotation for marking the target operating system for a Tsunami {@link * com.google.tsunami.plugin.VulnDetector} plugin. * *

If annotated by this annotation, the {@link com.google.tsunami.plugin.VulnDetector} will only * be executed by the scanner when the target network service is running on the described Operating * System. * *

Example usage: * *

{@code
 * {@literal @}ForOperatingSystemClass({
 *   vendor = "Linux",
 *   minAccuracy = 90
 * })
 * public class ExamplePlugin implements VulnDetector {
 *   // ...
 * }
 * }
*/ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface ForOperatingSystemClass { // The vendor of the target operating system, e.g. "Microsoft" String[] vendor() default ""; // The family of the target operating system, e.g. "Windows" String[] osfamily() default ""; // The minimum accuracy of the target operating system, e.g. 90 int minAccuracy() default 0; } ================================================ FILE: plugin/src/main/java/com/google/tsunami/plugin/annotations/ForServiceName.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.plugin.annotations; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * An annotation for marking the target service name(s) for a Tsunami {@link * com.google.tsunami.plugin.VulnDetector} plugin. * *

If annotated by this annotation, the {@link com.google.tsunami.plugin.VulnDetector} will only * be executed by the scanner when the scan target exposed a network service whose name matches the * listed service name in the annotation. Network service names should genuinely follow those listed * at RFC6335. * * Example usage: * *

{@code
 * {@literal @}ForServiceName({"http", "https"})
 * public class ExamplePlugin implements VulnDetector {
 *   // ...
 * }
 * }
*/ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface ForServiceName { /** * Array of target network service names for a Tsunami {@link * com.google.tsunami.plugin.VulnDetector} plugin. The values for application layer protocols * should genuinely follow naming conventions listed at RFC6335. * * @return the targeted network service names for a Tsunami plugin. */ String[] value(); } ================================================ FILE: plugin/src/main/java/com/google/tsunami/plugin/annotations/ForSoftware.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.plugin.annotations; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * An annotation for marking the target software and target version for a Tsunami {@link * com.google.tsunami.plugin.VulnDetector} plugin. * *

If annotated by this annotation, the {@link com.google.tsunami.plugin.VulnDetector} will only * be executed by the scanner when the scan target is running the matching software behind a network * service. * * Example usage: * *

{@code
 * {@literal @}ForSoftware(
 *   name = "WordPress",
 *   versions = {
 *     "0.8",
 *     "0.9",
 *     "[1.3,2.0)"
 *   }
 * )
 * public class ExamplePlugin implements VulnDetector {
 *   // ...
 * }
 * }
*/ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface ForSoftware { /** * Name of the target software, case insensitive. * * @return target software name. */ // TODO(b/145315535): handle name conflicts, include other properties that uniquely identify // software. String name(); /** * Array of versions and version ranges of the target software. * *

Some version and version range examples are: * *

    *
  • 1.0 Version 1.0. *
  • [1.0,2.0) Version 1.0 (inclusive) to 2.0 (exclusive). *
  • [1.0,2.0] Version 1.0 to 2.0 (both inclusive). *
  • [1.0,) Version 1.0 (inclusive) and higher. *
  • (,1.0] Version 1.0 (inclusive) and lower. *
* * Example value for this field: * *
    *
  • [ 1.0 ] *
  • [ 1.0, [1.1, 1.5) ] *
  • [ 1.0, [1.1, 1.5), 1.7, 1.8 ] *
  • [ (,1.0], [1.1, 1.3), [1.4,) ] *
* * @return target software versions. */ String[] versions() default {}; } ================================================ FILE: plugin/src/main/java/com/google/tsunami/plugin/annotations/ForWebService.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.plugin.annotations; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * An util annotation that is shorthand of {@code {@literal @}ForServiceName({"http", "https", * ...})}, marking that the intended network services of a plugin are web services. */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface ForWebService {} ================================================ FILE: plugin/src/main/java/com/google/tsunami/plugin/annotations/PluginInfo.java ================================================ /* * Copyright 2019 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.plugin.annotations; import com.google.tsunami.plugin.PluginBootstrapModule; import com.google.tsunami.plugin.PluginType; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * An annotation for adding related information about a Tsunami plugin. * * Example usage: * *
{@code
 * {@literal @}PluginInfo(
 *   type = PluginType.VULN_DETECTOR,
 *   name = "example_plugin",
 *   version = "1.0",
 *   description = "An example plugin that demonstrates the usage of the PluginInfo annotation",
 *   author = "Author A (a@example.com)",
 *   bootstrapModule = ExamplePluginBootstrapModule.class})
 * public class ExamplePlugin implements VulnDetector {
 *   // ...
 * }
 * }
*/ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface PluginInfo { /** * The type of this plugin. * * @return Tsunami plugin type. */ PluginType type(); /** * The short name of this plugin. * *

Convention: use {@code lower_snake_case} style (lowercase words separated by * underscores) for this name. * * @return Tsunami plugin name. */ // TODO(magl): add compile time style validation. String name(); /** * The version of this plugin. * *

Convention: follow the {@code major.minor} version scheme. * * @return Tsunami plugin version. */ // TODO(magl): add compile time style validation. String version(); /** * Detailed description of this plugin. * *

In general, the description of a plugin should tell the purpose of this plugin. For example, * for a {@code VULN_DETECTOR}, the description should mention the affected software, * vulnerability, affected version ranges, etc. * * @return Tsunami plugin description. */ String description(); /** * Author of this plugin. * *

Convention: "Name (contact email)". For example, {@code Alice (Alice@example.com)}. * * @return Tsunami plugin author. */ // TODO(magl): add compile time style validation. String author(); /** * Module for bootstrapping this plugin. * * @return Tsunami plugin's bootstrap module class. */ Class bootstrapModule(); } ================================================ FILE: plugin/src/main/java/com/google/tsunami/plugin/annotations/RequiresCallbackServer.java ================================================ /* * Copyright 2024 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.plugin.annotations; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * Annotation for marking that a plugin can only run if the callback server is enabled. */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface RequiresCallbackServer {} ================================================ FILE: plugin/src/main/java/com/google/tsunami/plugin/payload/NotImplementedException.java ================================================ /* * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.plugin.payload; import com.google.errorprone.annotations.FormatMethod; import com.google.errorprone.annotations.FormatString; /** * Thrown whenever a {@link com.google.tsunami.proto.PayloadGeneratorConfig} results in a * combination that does not have a payload. * *

To reduce the burden on callers, this is an unchecked exception. The goal is simply to * notify the developer that the payload generator cannot be used in the requested context. If the * generator does work in the requested context, this exception would never be thrown. */ public final class NotImplementedException extends RuntimeException { @FormatMethod public NotImplementedException(@FormatString String format, Object... args) { super(String.format(format, args)); } } ================================================ FILE: plugin/src/main/java/com/google/tsunami/plugin/payload/Payload.java ================================================ /* * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.plugin.payload; import com.google.common.flogger.GoogleLogger; import com.google.protobuf.ByteString; import com.google.tsunami.proto.PayloadAttributes; import com.google.tsunami.proto.PayloadGeneratorConfig; import java.util.Optional; /** Type returned by {@link PayloadGenerator} to be used in detectors. */ public class Payload { private static final GoogleLogger logger = GoogleLogger.forEnclosingClass(); private final String payload; private final Validator validator; private final PayloadAttributes attributes; private final PayloadGeneratorConfig config; public Payload( String payload, Validator validator, PayloadAttributes attributes, PayloadGeneratorConfig config) { this.payload = payload; this.validator = validator; this.attributes = attributes; this.config = config; } /** * Get the string representation of the payload. * * @return the actual payload string */ public final String getPayload() { logger.atInfo().log( "%s generated payload `%s`, %s use the callback server", this.config, this.payload, this.attributes.getUsesCallbackServer() ? "does" : "does not"); return this.payload; } /** * Checks if the supplied payload was executed based on a given input e.g. a reflective RCE. * * @param input - a UTF-8 encoded string * @return whether this payload is executed on the scan target. */ public final boolean checkIfExecuted(String input) { return this.validator.isExecuted(Optional.of(ByteString.copyFromUtf8(input))); } /** * Checks if the supplied payload was executed based on a given input e.g. a reflective RCE. * * @param input - a sequence of bytes in the {@link ByteString} format. * @return whether this payload is executed on the scan target. */ public final boolean checkIfExecuted(ByteString input) { return this.validator.isExecuted(Optional.of(input)); } /** * Checks if the supplied payload was executed based on a given input e.g. a reflective RCE. * * @param input - an optional sequence of bytes in the {@link ByteString} format. * @return whether this payload is executed on the scan target. */ public final boolean checkIfExecuted(Optional input) { return this.validator.isExecuted(input); } /** * Checks if the supplied payload was executed without supplying an input e.g. validation against * the callback server does not require input. * * @return whether this payload is executed on the scan target. */ public final boolean checkIfExecuted() { return this.validator.isExecuted(Optional.empty()); } /** * Get additional attributes about this payload. * * @return the {@link PayloadAttributes} about this payload */ public final PayloadAttributes getPayloadAttributes() { return this.attributes; } } ================================================ FILE: plugin/src/main/java/com/google/tsunami/plugin/payload/PayloadGenerator.java ================================================ /* * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.plugin.payload; import static com.google.common.base.Preconditions.checkNotNull; import static java.lang.annotation.RetentionPolicy.RUNTIME; import com.google.common.collect.ImmutableList; import com.google.protobuf.ByteString; import com.google.tsunami.plugin.TcsClient; import com.google.tsunami.proto.PayloadAttributes; import com.google.tsunami.proto.PayloadDefinition; import com.google.tsunami.proto.PayloadGeneratorConfig; import java.lang.annotation.Retention; import java.util.Optional; import javax.inject.Inject; import javax.inject.Qualifier; /** Holds the generate function to get a detection payload given config parameters */ public final class PayloadGenerator { private static final int SECRET_LENGTH = 8; private static final String TOKEN_CALLBACK_SERVER_URL = "$TSUNAMI_PAYLOAD_TOKEN_URL"; private static final String TOKEN_RANDOM_STRING = "$TSUNAMI_PAYLOAD_TOKEN_RANDOM"; private final TcsClient tcsClient; private final PayloadSecretGenerator secretGenerator; private final ImmutableList payloads; @Inject PayloadGenerator( TcsClient tcsClient, PayloadSecretGenerator secretGenerator, @Payloads ImmutableList payloads) { this.tcsClient = checkNotNull(tcsClient); this.secretGenerator = checkNotNull(secretGenerator); this.payloads = checkNotNull(payloads); } public boolean isCallbackServerEnabled() { return tcsClient.isCallbackServerEnabled(); } /** * Returns a {@link Payload} for a given {@link PayloadGeneratorConfig}. * *

The framework prioritizes finding a callback server payload if callback server is enabled * and falls back to any payload that matches. * * @param config configurations to the payload generator * @return the generated {@link Payload} based on the given {@code config} */ public Payload generate(PayloadGeneratorConfig config) { return generatePayload(config, /* enforceNoCallback= */ false); } public Payload generateNoCallback(PayloadGeneratorConfig config) { return generatePayload(config, /* enforceNoCallback= */ true); } private Payload generatePayload(PayloadGeneratorConfig config, boolean enforceNoCallback) { PayloadDefinition selectedPayload = null; if (tcsClient.isCallbackServerEnabled() && !enforceNoCallback) { for (PayloadDefinition candidate : payloads) { if (isMatchingPayload(candidate, config) && candidate.getUsesCallbackServer().getValue()) { selectedPayload = candidate; break; } } } if (selectedPayload == null) { for (PayloadDefinition candidate : payloads) { if (isMatchingPayload(candidate, config) && !candidate.getUsesCallbackServer().getValue()) { selectedPayload = candidate; break; } } } if (selectedPayload == null) { throw new NotImplementedException( "No payload implemented for %s vulnerability type, %s interpretation environment, %s" + " execution environment", config.getVulnerabilityType(), config.getInterpretationEnvironment(), config.getExecutionEnvironment()); } return convertParsedPayload(selectedPayload, config); } private boolean isMatchingPayload(PayloadDefinition p, PayloadGeneratorConfig c) { return p.getVulnerabilityTypeList().contains(c.getVulnerabilityType()) && p.getInterpretationEnvironment() == c.getInterpretationEnvironment() && p.getExecutionEnvironment() == c.getExecutionEnvironment(); } private Payload convertParsedPayload(PayloadDefinition p, PayloadGeneratorConfig c) { String secret = secretGenerator.generate(SECRET_LENGTH); if (p.getUsesCallbackServer().getValue()) { return new Payload( p.getPayloadString() .getValue() .replace(TOKEN_CALLBACK_SERVER_URL, tcsClient.getCallbackUri(secret)), (Validator) (unused) -> tcsClient.hasOobLog(secret), PayloadAttributes.newBuilder().setUsesCallbackServer(true).build(), c); } else { String payloadString = p.getPayloadString().getValue().replace(TOKEN_RANDOM_STRING, secret); Validator v; switch (p.getValidationType()) { case VALIDATION_REGEX: String processedRegex = p.getValidationRegex().getValue().replace(TOKEN_RANDOM_STRING, secret); v = (Validator) (Optional input) -> input.map(i -> i.toStringUtf8().matches(processedRegex)).orElse(false); return new Payload( payloadString, v, PayloadAttributes.newBuilder().setUsesCallbackServer(false).build(), c); default: throw new NotImplementedException( "Validation type %s not implemented.", p.getValidationType()); } } } /** Guice interface for injecting parsed payloads from payload_definitions.yaml */ @Qualifier @Retention(RUNTIME) public @interface Payloads {} } ================================================ FILE: plugin/src/main/java/com/google/tsunami/plugin/payload/PayloadGeneratorModule.java ================================================ /* * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.plugin.payload; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import static java.nio.charset.StandardCharsets.UTF_8; import com.google.common.collect.ImmutableList; import com.google.common.io.Resources; import com.google.common.net.InetAddresses; import com.google.common.net.InternetDomainName; import com.google.gson.Gson; import com.google.inject.AbstractModule; import com.google.inject.Provides; import com.google.protobuf.util.JsonFormat; import com.google.tsunami.common.net.http.HttpClient; import com.google.tsunami.plugin.TcsClient; import com.google.tsunami.plugin.TcsClientCliOptions; import com.google.tsunami.plugin.TcsConfigProperties; import com.google.tsunami.proto.PayloadDefinition; import com.google.tsunami.proto.PayloadGeneratorConfig; import com.google.tsunami.proto.PayloadLibrary; import com.google.tsunami.proto.PayloadValidationType; import java.io.IOException; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.security.SecureRandom; import java.util.List; import java.util.Map; import javax.annotation.Nullable; import javax.inject.Qualifier; import javax.inject.Singleton; import org.yaml.snakeyaml.LoaderOptions; import org.yaml.snakeyaml.Yaml; import org.yaml.snakeyaml.constructor.SafeConstructor; /** Guice module for installing {@link PayloadGenerator}. */ public final class PayloadGeneratorModule extends AbstractModule { private final SecureRandom secureRng; public PayloadGeneratorModule(SecureRandom secureRng) { this.secureRng = secureRng; } @Qualifier @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD}) @interface CallbackAddress {} @Qualifier @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD}) @interface CallbackPort {} @Qualifier @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD}) @interface CallbackPollingUri {} @Provides @CallbackAddress String providesCallbackAddress(TcsConfigProperties config, TcsClientCliOptions cliOptions) { if (cliOptions.callbackAddress != null) { return cliOptions.callbackAddress; } return config.callbackAddress; } @Provides @CallbackPort Integer providesCallbackPort(TcsConfigProperties config, TcsClientCliOptions cliOptions) { if (cliOptions.callbackPort != null) { return cliOptions.callbackPort; } return config.callbackPort; } @Provides @CallbackPollingUri String providesCallbackPollingUri(TcsConfigProperties config, TcsClientCliOptions cliOptions) { if (cliOptions.pollingUri != null) { return cliOptions.pollingUri; } return config.pollingUri; } @Provides TcsClient providesTcsClient( @Nullable @CallbackAddress String callbackAddress, @Nullable @CallbackPort Integer callbackPort, @Nullable @CallbackPollingUri String pollingUri, HttpClient httpClient) { // when all tcs config are not set, we provide an invalid {@link TcsClient} // so that {@link TcsClient#isCallbackServerEnabled} returns false. if (callbackAddress == null && callbackPort == null && pollingUri == null) { return new TcsClient("", 0, "", checkNotNull(httpClient)); } checkNotNull(callbackAddress); checkNotNull(callbackPort); checkNotNull(pollingUri); checkArgument( InetAddresses.isInetAddress(callbackAddress) || InternetDomainName.isValid(callbackAddress), "Invalid callback address specified"); checkArgument(callbackPort > 0 && callbackPort < 65536, "Invalid port number specified"); return new TcsClient(callbackAddress, callbackPort, pollingUri, checkNotNull(httpClient)); } @Provides PayloadSecretGenerator providesPayloadSecretGenerator() { return new PayloadSecretGenerator(this.secureRng); } @Provides @PayloadGenerator.Payloads @Singleton ImmutableList provideParsedPayloads() throws IOException { // It is only safe to use SnakeYaml with SafeConstructor. // Parse the YAML by converting it into JSON and then into the proto message Yaml yaml = new Yaml(new SafeConstructor(new LoaderOptions())); Map rawYamlData = yaml.load( Resources.toString( Resources.getResource(this.getClass(), "payload_definitions.yaml"), UTF_8)); Gson gson = new Gson(); String json = gson.toJson(rawYamlData); PayloadLibrary.Builder builder = PayloadLibrary.newBuilder(); JsonFormat.parser().ignoringUnknownFields().merge(json, builder); PayloadLibrary parsed = builder.build(); return validatePayloads(parsed.getPayloadsList()); } /** Validates that the parsed payloads adhere to the defined schema. */ ImmutableList validatePayloads(List payloads) { for (PayloadDefinition p : payloads) { checkArgument(p.hasName(), "Parsed payload does not have a name."); checkArgument( p.getInterpretationEnvironment() != PayloadGeneratorConfig.InterpretationEnvironment .INTERPRETATION_ENVIRONMENT_UNSPECIFIED, "Parsed payload does not have an interpretation_environment."); checkArgument( p.getExecutionEnvironment() != PayloadGeneratorConfig.ExecutionEnvironment.EXECUTION_ENVIRONMENT_UNSPECIFIED, "Parsed payload does not have an exeuction_environment."); checkArgument( !p.getVulnerabilityTypeList().isEmpty(), "Parsed payload has no entries for vulnerability_type."); checkArgument(p.hasPayloadString(), "Parsed payload does not have a payload_string."); if (p.getUsesCallbackServer().getValue()) { checkArgument( p.getPayloadString().getValue().contains("$TSUNAMI_PAYLOAD_TOKEN_URL"), "Parsed payload uses callback server but $TSUNAMI_PAYLOAD_TOKEN_URL not found in" + " payload_string."); } else { checkArgument( p.getValidationType() != PayloadValidationType.VALIDATION_TYPE_UNSPECIFIED, "Parsed payload has no validation_type and does not use the callback server."); if (p.getValidationType() == PayloadValidationType.VALIDATION_REGEX) { checkArgument( p.hasValidationRegex(), "Parsed payload has no validation_regex but uses PayloadValidationType.REGEX"); } } } return ImmutableList.copyOf(payloads); } } ================================================ FILE: plugin/src/main/java/com/google/tsunami/plugin/payload/PayloadSecretGenerator.java ================================================ /* * Copyright 2021 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.plugin.payload; import com.google.common.io.BaseEncoding; import com.google.inject.AbstractModule; import com.google.inject.Provides; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.security.SecureRandom; import javax.inject.Inject; import javax.inject.Qualifier; import javax.inject.Singleton; /** Generates secrets used in the Payload generation framework */ public final class PayloadSecretGenerator { private final SecureRandom payloadSecretRng; @Inject PayloadSecretGenerator(@PayloadSecretRng SecureRandom payloadSecretRng) { this.payloadSecretRng = payloadSecretRng; } public String generate(int secretLength) { byte[] randomBytes = new byte[secretLength]; payloadSecretRng.nextBytes(randomBytes); return BaseEncoding.base16().lowerCase().encode(randomBytes); } public static PayloadSecretGeneratorModule getModule() { return new PayloadSecretGeneratorModule(); } private static final class PayloadSecretGeneratorModule extends AbstractModule { @Provides @PayloadSecretRng @Singleton SecureRandom providesPayloadSecretRng() { return new SecureRandom(); } } /** * Interface for Guice binding annotation for the {@link PayloadSecretGenerator}'s SecureRandom */ @Qualifier @Retention(RetentionPolicy.RUNTIME) public @interface PayloadSecretRng {} } ================================================ FILE: plugin/src/main/java/com/google/tsunami/plugin/payload/README.md ================================================ # Tsunami Payload Generation Framework This is the code for Tsunami's payload generation framework, an optional library for detectors which automatically selects the best payload for a given vulnerability, taking out the guesswork when writing a new detector, reducing false positives, and standardizing payloads across detectors. It is also the interface for using the [Tsunami Callback Server](https://github.com/google/tsunami-security-scanner-callback-server). Detectors targeting remote code executions (RCE) and server-side request forgery (SSRF) vulnerabilities are ideal candidates for using the payload framework. For an example of how to use the framework, see [the example plugin](https://github.com/google/tsunami-security-scanner-plugins/tree/master/examples/example_payload_framework_vuln_detector). ## payload_definitions.yaml [payload_definitions.yaml](https://github.com/google/tsunami-security-scanner/blob/master/plugin/src/main/resources/com/google/tsunami/plugin/payload/payload_definitions.yaml) defines the actual payloads used in the payload generation framework. See the schema definition in [payload_generator.proto](https://github.com/google/tsunami-security-scanner/blob/master/proto/payload_generator.proto). When adding a new payload definition, make sure to add [test cases](https://github.com/google/tsunami-security-scanner/blob/master/plugin/src/test/java/com/google/tsunami/plugin/payload/PayloadGeneratorTest.java). ================================================ FILE: plugin/src/main/java/com/google/tsunami/plugin/payload/Validator.java ================================================ /* * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.plugin.payload; import com.google.protobuf.ByteString; import java.util.Optional; /** Type used for functions which verify if a payload was executed */ @FunctionalInterface public interface Validator { /** * Checks whether the payload is executed. * * @param input - an optional sequence of bytes in the {@link ByteString} format. * @return whether a payload is executed on the scan target. */ boolean isExecuted(Optional input); } ================================================ FILE: plugin/src/main/java/com/google/tsunami/plugin/payload/testing/FakePayloadGeneratorModule.java ================================================ /* * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.plugin.payload.testing; import com.google.auto.value.AutoBuilder; import com.google.inject.AbstractModule; import com.google.tsunami.plugin.TcsConfigProperties; import com.google.tsunami.plugin.payload.PayloadGenerator; import com.google.tsunami.plugin.payload.PayloadGeneratorModule; import java.security.SecureRandom; import java.util.Optional; import okhttp3.mockwebserver.MockWebServer; import org.checkerframework.checker.nullness.qual.Nullable; /** * Guice module for interacting with {@link PayloadGenerator} in tests. Use {@link * FakePayloadGeneratorModule.Builder} instead of this directly. */ public final class FakePayloadGeneratorModule extends AbstractModule { private final TcsConfigProperties tcsConfig = new TcsConfigProperties(); private final SecureRandom secureRng; /** * @param callbackServer - if supplied, enables the payload generator to use the callback server. * If this behavior is unwanted, leave this empty. * @param secureRng - if you do not need control over the output of {@link SecureRandom#nextBytes} * in tests, leave this empty. */ FakePayloadGeneratorModule( Optional callbackServer, Optional secureRng) { this.tcsConfig.callbackAddress = callbackServer.map(c -> c.getHostName()).orElse(null); this.tcsConfig.callbackPort = callbackServer.map(c -> c.getPort()).orElse(null); this.tcsConfig.pollingUri = callbackServer.map(c -> c.url("/").toString()).orElse(null); this.secureRng = secureRng.orElse(new SecureRandom()); } @Override protected void configure() { install(new PayloadGeneratorModule(secureRng)); bind(TcsConfigProperties.class).toInstance(tcsConfig); } /** * Creates a builder for the {@link FakePayloadGeneratorModule}. * * @return a builder for configuring the module */ public static Builder builder() { return Builder.builder(); } static FakePayloadGeneratorModule build( @Nullable MockWebServer callbackServer, @Nullable SecureRandom secureRng) { return new FakePayloadGeneratorModule( Optional.ofNullable(callbackServer), Optional.ofNullable(secureRng)); } /** Configures {@link FakePayloadGeneratorModule}. */ @AutoBuilder(callMethod = "build") public abstract static class Builder { public static Builder builder() { return new AutoBuilder_FakePayloadGeneratorModule_Builder(); } public abstract Builder setCallbackServer(MockWebServer callbackServer); public abstract Builder setSecureRng(SecureRandom secureRng); public abstract FakePayloadGeneratorModule build(); } } ================================================ FILE: plugin/src/main/java/com/google/tsunami/plugin/payload/testing/PayloadTestHelper.java ================================================ /* * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.plugin.payload.testing; import com.google.protobuf.util.JsonFormat; import com.google.tsunami.callbackserver.proto.PollingResult; import com.google.tsunami.common.net.http.HttpStatus; import java.io.IOException; import okhttp3.mockwebserver.MockResponse; /** Exposes some helpers when writing tests against code that use the paylaod generator. */ public final class PayloadTestHelper { private PayloadTestHelper() {} public static MockResponse generateMockSuccessfulCallbackResponse() throws IOException { PollingResult log = PollingResult.newBuilder().setHasHttpInteraction(true).build(); String body = JsonFormat.printer().preservingProtoFieldNames().print(log); return new MockResponse().setResponseCode(HttpStatus.OK.code()).setBody(body); } public static MockResponse generateMockUnsuccessfulCallbackResponse() { return new MockResponse().setResponseCode(HttpStatus.NOT_FOUND.code()); } } ================================================ FILE: plugin/src/main/java/com/google/tsunami/plugin/testing/FailedPortScanner.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.plugin.testing; import com.google.tsunami.plugin.PluginType; import com.google.tsunami.plugin.PortScanner; import com.google.tsunami.plugin.annotations.PluginInfo; import com.google.tsunami.proto.PortScanningReport; import com.google.tsunami.proto.ScanTarget; /** A fake PortScanner plugin that instantly fails for testing purpose only. */ @PluginInfo( type = PluginType.PORT_SCAN, name = "FailedPortScanner", version = "v0.1", description = "A fake PortScanner that instantly fails.", author = "fake", bootstrapModule = FailedPortScannerBootstrapModule.class) public final class FailedPortScanner implements PortScanner { @Override public PortScanningReport scan(ScanTarget scanTarget) { throw new RuntimeException("PortScanner failed"); } } ================================================ FILE: plugin/src/main/java/com/google/tsunami/plugin/testing/FailedPortScannerBootstrapModule.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.plugin.testing; import com.google.tsunami.plugin.PluginBootstrapModule; /** Bootstrapping module for {@link FailedPortScanner}. */ public final class FailedPortScannerBootstrapModule extends PluginBootstrapModule { @Override protected void configurePlugin() { registerPlugin(FailedPortScanner.class); } } ================================================ FILE: plugin/src/main/java/com/google/tsunami/plugin/testing/FailedRemoteVulnDetector.java ================================================ /* * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.plugin.testing; import com.google.common.collect.ImmutableList; import com.google.common.collect.Sets; import com.google.tsunami.plugin.LanguageServerException; import com.google.tsunami.plugin.PluginType; import com.google.tsunami.plugin.RemoteVulnDetector; import com.google.tsunami.plugin.annotations.PluginInfo; import com.google.tsunami.proto.DetectionReportList; import com.google.tsunami.proto.MatchedPlugin; import com.google.tsunami.proto.NetworkService; import com.google.tsunami.proto.PluginDefinition; import com.google.tsunami.proto.TargetInfo; import com.google.tsunami.proto.Vulnerability; import java.util.Set; /** Fake {@link RemoteVulnDetector} implementation that fails to run. */ @PluginInfo( type = PluginType.REMOTE_VULN_DETECTION, name = "FailedRemoteVulnDetector", version = "v0.1", description = "fake description", author = "fake", bootstrapModule = FailedRemoteVulnDetectorBootstrapModule.class) public final class FailedRemoteVulnDetector implements RemoteVulnDetector { private final Set matchedPluginsToRun; public FailedRemoteVulnDetector() { this.matchedPluginsToRun = Sets.newHashSet(); } @Override public ImmutableList getAdvisories() { return ImmutableList.of(); } @Override public DetectionReportList detect(TargetInfo target, ImmutableList services) { throw new LanguageServerException("RemoteVulnDetector failed."); } @Override public ImmutableList getAllPlugins() { return ImmutableList.of(PluginDefinition.getDefaultInstance()); } @Override public void addMatchedPluginToDetect(MatchedPlugin plugin) { this.matchedPluginsToRun.add(plugin); } } ================================================ FILE: plugin/src/main/java/com/google/tsunami/plugin/testing/FailedRemoteVulnDetectorBootstrapModule.java ================================================ /* * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.plugin.testing; import com.google.tsunami.plugin.PluginBootstrapModule; /** Bootstrapping module for {@link FailedRemoteVulnDetector}. */ public final class FailedRemoteVulnDetectorBootstrapModule extends PluginBootstrapModule { @Override protected void configurePlugin() { registerPlugin(FailedRemoteVulnDetector.class); } } ================================================ FILE: plugin/src/main/java/com/google/tsunami/plugin/testing/FailedServiceFingerprinter.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.plugin.testing; import com.google.tsunami.plugin.PluginType; import com.google.tsunami.plugin.ServiceFingerprinter; import com.google.tsunami.plugin.annotations.ForServiceName; import com.google.tsunami.plugin.annotations.PluginInfo; import com.google.tsunami.proto.FingerprintingReport; import com.google.tsunami.proto.NetworkService; import com.google.tsunami.proto.TargetInfo; /** A fake ServiceFingerprinter plugin that instantly fails for testing purpose only. */ @PluginInfo( type = PluginType.SERVICE_FINGERPRINT, name = "FailedServiceFingerprinter", version = "v0.1", description = "A fake ServiceFingerprinter that instantly fails.", author = "fake", bootstrapModule = FailedServiceFingerprinterBootstrapModule.class) @ForServiceName("http") public final class FailedServiceFingerprinter implements ServiceFingerprinter { @Override public FingerprintingReport fingerprint(TargetInfo targetInfo, NetworkService networkService) { throw new RuntimeException("ServiceFingerprinter failed"); } } ================================================ FILE: plugin/src/main/java/com/google/tsunami/plugin/testing/FailedServiceFingerprinterBootstrapModule.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.plugin.testing; import com.google.tsunami.plugin.PluginBootstrapModule; /** Bootstrapping module for {@link FailedServiceFingerprinter}. */ public final class FailedServiceFingerprinterBootstrapModule extends PluginBootstrapModule { @Override protected void configurePlugin() { registerPlugin(FailedServiceFingerprinter.class); } } ================================================ FILE: plugin/src/main/java/com/google/tsunami/plugin/testing/FailedVulnDetector.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.plugin.testing; import com.google.common.collect.ImmutableList; import com.google.tsunami.plugin.PluginType; import com.google.tsunami.plugin.VulnDetector; import com.google.tsunami.plugin.annotations.PluginInfo; import com.google.tsunami.proto.DetectionReportList; import com.google.tsunami.proto.NetworkService; import com.google.tsunami.proto.TargetInfo; import com.google.tsunami.proto.Vulnerability; /** A fake VulnDetector plugin that instantly fails for testing purpose only. */ @PluginInfo( type = PluginType.VULN_DETECTION, name = "FailedVulnDetector", version = "v0.1", description = "A fake VulnDetector that instantly fails.", author = "fake", bootstrapModule = FailedVulnDetectorBootstrapModule.class) public class FailedVulnDetector implements VulnDetector { @Override public ImmutableList getAdvisories() { return ImmutableList.of(); } @Override public DetectionReportList detect( TargetInfo targetInfo, ImmutableList matchedServices) { throw new RuntimeException("VulnDetector failed."); } } ================================================ FILE: plugin/src/main/java/com/google/tsunami/plugin/testing/FailedVulnDetectorBootstrapModule.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.plugin.testing; import com.google.tsunami.plugin.PluginBootstrapModule; /** Bootstrapping module for {@link FailedVulnDetector}. */ public final class FailedVulnDetectorBootstrapModule extends PluginBootstrapModule { @Override protected void configurePlugin() { registerPlugin(FailedVulnDetector.class); } } ================================================ FILE: plugin/src/main/java/com/google/tsunami/plugin/testing/FakePluginExecutionModule.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.plugin.testing; import com.google.common.util.concurrent.ListeningScheduledExecutorService; import com.google.common.util.concurrent.MoreExecutors; import com.google.inject.AbstractModule; import com.google.tsunami.plugin.PluginExecutionThreadPool; import com.google.tsunami.plugin.PluginExecutorModule; import java.util.concurrent.Executors; /** Installs dependencies used for plugin executions in unit tests. */ public final class FakePluginExecutionModule extends AbstractModule { @Override protected void configure() { install(new PluginExecutorModule()); bind(ListeningScheduledExecutorService.class) .annotatedWith(PluginExecutionThreadPool.class) .toInstance(MoreExecutors.listeningDecorator(Executors.newScheduledThreadPool(1))); } } ================================================ FILE: plugin/src/main/java/com/google/tsunami/plugin/testing/FakePortScanner.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.plugin.testing; import com.google.tsunami.common.data.NetworkEndpointUtils; import com.google.tsunami.plugin.PluginType; import com.google.tsunami.plugin.PortScanner; import com.google.tsunami.plugin.annotations.PluginInfo; import com.google.tsunami.proto.NetworkEndpoint; import com.google.tsunami.proto.NetworkService; import com.google.tsunami.proto.PortScanningReport; import com.google.tsunami.proto.ScanTarget; import com.google.tsunami.proto.TargetInfo; import com.google.tsunami.proto.TransportProtocol; /** A fake PortScanner plugin for testing purpose only. */ @PluginInfo( type = PluginType.PORT_SCAN, name = "FakePortScanner", version = "v0.1", description = "A fake PortScanner.", author = "fake", bootstrapModule = FakePortScannerBootstrapModule.class) public class FakePortScanner implements PortScanner { public static NetworkService getFakeNetworkService(NetworkEndpoint networkEndpoint) { return NetworkService.newBuilder() .setNetworkEndpoint(NetworkEndpointUtils.forNetworkEndpointAndPort(networkEndpoint, 80)) .setTransportProtocol(TransportProtocol.TCP) .setServiceName("http") .build(); } @Override public PortScanningReport scan(ScanTarget scanTarget) { return PortScanningReport.newBuilder() .setTargetInfo(TargetInfo.newBuilder().addNetworkEndpoints(scanTarget.getNetworkEndpoint())) .addNetworkServices(getFakeNetworkService(scanTarget.getNetworkEndpoint())) .build(); } } ================================================ FILE: plugin/src/main/java/com/google/tsunami/plugin/testing/FakePortScanner2.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.plugin.testing; import com.google.tsunami.common.data.NetworkEndpointUtils; import com.google.tsunami.plugin.PluginType; import com.google.tsunami.plugin.PortScanner; import com.google.tsunami.plugin.annotations.PluginInfo; import com.google.tsunami.proto.NetworkEndpoint; import com.google.tsunami.proto.NetworkService; import com.google.tsunami.proto.PortScanningReport; import com.google.tsunami.proto.ScanTarget; import com.google.tsunami.proto.Software; import com.google.tsunami.proto.TargetInfo; import com.google.tsunami.proto.TransportProtocol; /** A fake PortScanner plugin for testing purpose only. */ @PluginInfo( type = PluginType.PORT_SCAN, name = "FakePortScanner2", version = "v0.1", description = "Another fake PortScanner.", author = "fake", bootstrapModule = FakePortScannerBootstrapModule2.class) public class FakePortScanner2 implements PortScanner { public static NetworkService getFakeNetworkService(NetworkEndpoint networkEndpoint) { return NetworkService.newBuilder() .setNetworkEndpoint(NetworkEndpointUtils.forNetworkEndpointAndPort(networkEndpoint, 22)) .setTransportProtocol(TransportProtocol.TCP) .setServiceName("ssh") .setSoftware(Software.newBuilder().setName("FakeSoftware2")) .build(); } @Override public PortScanningReport scan(ScanTarget scanTarget) { return PortScanningReport.newBuilder() .setTargetInfo(TargetInfo.newBuilder().addNetworkEndpoints(scanTarget.getNetworkEndpoint())) .addNetworkServices(getFakeNetworkService(scanTarget.getNetworkEndpoint())) .build(); } } ================================================ FILE: plugin/src/main/java/com/google/tsunami/plugin/testing/FakePortScannerBootstrapModule.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.plugin.testing; import com.google.tsunami.plugin.PluginBootstrapModule; /** Bootstrapping module for {@link FakePortScanner}. */ public class FakePortScannerBootstrapModule extends PluginBootstrapModule { @Override protected void configurePlugin() { registerPlugin(FakePortScanner.class); } } ================================================ FILE: plugin/src/main/java/com/google/tsunami/plugin/testing/FakePortScannerBootstrapModule2.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.plugin.testing; import com.google.tsunami.plugin.PluginBootstrapModule; /** Bootstrapping module for {@link FakePortScanner2}. */ public class FakePortScannerBootstrapModule2 extends PluginBootstrapModule { @Override protected void configurePlugin() { registerPlugin(FakePortScanner2.class); } } ================================================ FILE: plugin/src/main/java/com/google/tsunami/plugin/testing/FakePortScannerBootstrapModuleEmpty.java ================================================ /* * Copyright 2025 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.plugin.testing; import com.google.tsunami.plugin.PluginBootstrapModule; /** Bootstrapping module for {@link FakePortScannerEmpty}. */ public class FakePortScannerBootstrapModuleEmpty extends PluginBootstrapModule { @Override protected void configurePlugin() { registerPlugin(FakePortScannerEmpty.class); } } ================================================ FILE: plugin/src/main/java/com/google/tsunami/plugin/testing/FakePortScannerEmpty.java ================================================ /* * Copyright 2025 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.plugin.testing; import com.google.tsunami.plugin.PluginType; import com.google.tsunami.plugin.PortScanner; import com.google.tsunami.plugin.annotations.PluginInfo; import com.google.tsunami.proto.PortScanningReport; import com.google.tsunami.proto.ScanTarget; import com.google.tsunami.proto.TargetInfo; /** A fake PortScanner plugin for testing purpose only. */ @PluginInfo( type = PluginType.PORT_SCAN, name = "FakePortScannerEmpty", version = "v0.1", description = "Another fake PortScanner.", author = "fake", bootstrapModule = FakePortScannerBootstrapModuleEmpty.class) public class FakePortScannerEmpty implements PortScanner { @Override public PortScanningReport scan(ScanTarget scanTarget) { return PortScanningReport.newBuilder() .setTargetInfo(TargetInfo.newBuilder().addNetworkEndpoints(scanTarget.getNetworkEndpoint())) .build(); } } ================================================ FILE: plugin/src/main/java/com/google/tsunami/plugin/testing/FakeRemoteVulnDetector.java ================================================ /* * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.plugin.testing; import static com.google.common.collect.ImmutableList.toImmutableList; import com.google.common.collect.ImmutableList; import com.google.common.collect.Sets; import com.google.protobuf.util.Timestamps; import com.google.tsunami.plugin.PluginType; import com.google.tsunami.plugin.RemoteVulnDetector; import com.google.tsunami.proto.DetectionReport; import com.google.tsunami.proto.DetectionReportList; import com.google.tsunami.proto.DetectionStatus; import com.google.tsunami.proto.MatchedPlugin; import com.google.tsunami.proto.NetworkService; import com.google.tsunami.proto.PluginDefinition; import com.google.tsunami.proto.PluginInfo; import com.google.tsunami.proto.Severity; import com.google.tsunami.proto.TargetInfo; import com.google.tsunami.proto.Vulnerability; import com.google.tsunami.proto.VulnerabilityId; import java.util.Set; /** * Fake {@link RemoteVulnDetector} implementation that only contains one {@link PluginDefinition} * proto available to run. */ @com.google.tsunami.plugin.annotations.PluginInfo( type = PluginType.REMOTE_VULN_DETECTION, name = "FakeRemoteVulnDetector", version = "v0.1", description = "fake description", author = "fake", bootstrapModule = FakeRemoteVulnDetectorBootstrapModule.class) public final class FakeRemoteVulnDetector implements RemoteVulnDetector { private final Set matchedPluginsToRun; // Used when multiple instances of this {@link RemoteVulnDetector} are created. private final int fakePluginId; public FakeRemoteVulnDetector() { this(0); } public FakeRemoteVulnDetector(int fakePluginId) { this.fakePluginId = fakePluginId; this.matchedPluginsToRun = Sets.newHashSet(); } @Override public ImmutableList getAdvisories() { return getAdvisoriesStatic(); } @Override public DetectionReportList detect(TargetInfo target, ImmutableList services) { ImmutableList> detectionReports = matchedPluginsToRun.stream() .map( plugin -> plugin.getServicesList().stream() .map(service -> getFakeDetectionReport(target, service)) .collect(toImmutableList())) .collect(toImmutableList()); var reportListBuilder = DetectionReportList.newBuilder(); detectionReports.forEach(reportListBuilder::addAllDetectionReports); return reportListBuilder.build(); } public static DetectionReport getFakeDetectionReport( TargetInfo targetInfo, NetworkService networkService) { return DetectionReport.newBuilder() .setTargetInfo(targetInfo) .setNetworkService(networkService) .setDetectionTimestamp(Timestamps.fromMillis(1234567890L)) .setDetectionStatus(DetectionStatus.VULNERABILITY_VERIFIED) .setVulnerability(getAdvisoriesStatic().get(0)) .build(); } @Override public ImmutableList getAllPlugins() { return ImmutableList.of( PluginDefinition.newBuilder() .setInfo( PluginInfo.newBuilder() .setType(PluginInfo.PluginType.VULN_DETECTION) .setName("FakeRemoteVuln" + fakePluginId) .setVersion("v0.1") .setDescription("FakeRemoteDescription" + fakePluginId) .setAuthor("fake")) .build()); } @Override public void addMatchedPluginToDetect(MatchedPlugin plugin) { this.matchedPluginsToRun.add(plugin); } private static ImmutableList getAdvisoriesStatic() { return ImmutableList.of( Vulnerability.newBuilder() .setMainId( VulnerabilityId.newBuilder().setPublisher("GOOGLE").setValue("FakeRemoteVuln")) .setSeverity(Severity.CRITICAL) .setTitle("FakeTitle") .setDescription("FakeRemoteDescription") .build()); } } ================================================ FILE: plugin/src/main/java/com/google/tsunami/plugin/testing/FakeRemoteVulnDetectorBootstrapModule.java ================================================ /* * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.plugin.testing; import com.google.tsunami.plugin.PluginBootstrapModule; /** * Bootstrapping module for {@link FakeRemoteVulnDetector}. Use this if there is need for only one * fake RemoteVulnDetector, otherwise prefer making a fake RemoteVulnDetectorLoadingModule to load * multiple instances. */ public final class FakeRemoteVulnDetectorBootstrapModule extends PluginBootstrapModule { @Override protected void configurePlugin() { registerPlugin(FakeRemoteVulnDetector.class); } } ================================================ FILE: plugin/src/main/java/com/google/tsunami/plugin/testing/FakeServiceFingerprinter.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.plugin.testing; import com.google.tsunami.plugin.PluginType; import com.google.tsunami.plugin.ServiceFingerprinter; import com.google.tsunami.plugin.annotations.ForServiceName; import com.google.tsunami.plugin.annotations.PluginInfo; import com.google.tsunami.proto.FingerprintingReport; import com.google.tsunami.proto.NetworkService; import com.google.tsunami.proto.ServiceContext; import com.google.tsunami.proto.Software; import com.google.tsunami.proto.TargetInfo; import com.google.tsunami.proto.WebServiceContext; /** A fake ServiceFingerprinter plugin for testing purpose only. */ @PluginInfo( type = PluginType.SERVICE_FINGERPRINT, name = "FakeServiceFingerprinter", version = "v0.1", description = "A fake ServiceFingerprinter.", author = "fake", bootstrapModule = FakeServiceFingerprinterBootstrapModule.class) @ForServiceName("http") public class FakeServiceFingerprinter implements ServiceFingerprinter { public static final Software IDENTIFIED_SOFTWARE = Software.newBuilder().setName("Jenkins").build(); public static NetworkService addWebServiceContext(NetworkService networkService) { return networkService.toBuilder() .setServiceContext( ServiceContext.newBuilder() .setWebServiceContext( WebServiceContext.newBuilder().setSoftware(IDENTIFIED_SOFTWARE))) .build(); } @Override public FingerprintingReport fingerprint(TargetInfo targetInfo, NetworkService networkService) { return FingerprintingReport.newBuilder() .addNetworkServices(addWebServiceContext(networkService)) .build(); } } ================================================ FILE: plugin/src/main/java/com/google/tsunami/plugin/testing/FakeServiceFingerprinterBootstrapModule.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.plugin.testing; import com.google.tsunami.plugin.PluginBootstrapModule; /** Bootstrapping module for {@link FakeServiceFingerprinter}. */ public class FakeServiceFingerprinterBootstrapModule extends PluginBootstrapModule { @Override protected void configurePlugin() { registerPlugin(FakeServiceFingerprinter.class); } } ================================================ FILE: plugin/src/main/java/com/google/tsunami/plugin/testing/FakeVulnDetector.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.plugin.testing; import static com.google.common.collect.ImmutableList.toImmutableList; import com.google.common.collect.ImmutableList; import com.google.protobuf.util.Timestamps; import com.google.tsunami.plugin.PluginType; import com.google.tsunami.plugin.VulnDetector; import com.google.tsunami.plugin.annotations.PluginInfo; import com.google.tsunami.proto.DetectionReport; import com.google.tsunami.proto.DetectionReportList; import com.google.tsunami.proto.DetectionStatus; import com.google.tsunami.proto.NetworkService; import com.google.tsunami.proto.Severity; import com.google.tsunami.proto.TargetInfo; import com.google.tsunami.proto.Vulnerability; import com.google.tsunami.proto.VulnerabilityId; /** A fake VulnDetector plugin for testing purpose only. */ @PluginInfo( type = PluginType.VULN_DETECTION, name = "FakeVulnDetector", version = "v0.1", description = "A fake VulnDetector.", author = "fake", bootstrapModule = FakeVulnDetectorBootstrapModule.class) public class FakeVulnDetector implements VulnDetector { public static DetectionReport getFakeDetectionReport( TargetInfo targetInfo, NetworkService networkService) { return DetectionReport.newBuilder() .setTargetInfo(targetInfo) .setNetworkService(networkService) .setDetectionTimestamp(Timestamps.fromMillis(1234567890L)) .setDetectionStatus(DetectionStatus.VULNERABILITY_VERIFIED) .setVulnerability(getAdvisoriesStatic().get(0)) .build(); } @Override public ImmutableList getAdvisories() { return getAdvisoriesStatic(); } @Override public DetectionReportList detect( TargetInfo targetInfo, ImmutableList matchedServices) { return DetectionReportList.newBuilder() .addAllDetectionReports( matchedServices.stream() .map(networkService -> getFakeDetectionReport(targetInfo, networkService)) .collect(toImmutableList())) .build(); } private static ImmutableList getAdvisoriesStatic() { return ImmutableList.of( Vulnerability.newBuilder() .setMainId(VulnerabilityId.newBuilder().setPublisher("GOOGLE").setValue("FakeVuln1")) .setSeverity(Severity.CRITICAL) .setTitle("FakeTitle1") .setDescription("FakeDescription1") .build()); } } ================================================ FILE: plugin/src/main/java/com/google/tsunami/plugin/testing/FakeVulnDetector2.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.plugin.testing; import static com.google.common.collect.ImmutableList.toImmutableList; import com.google.common.collect.ImmutableList; import com.google.protobuf.util.Timestamps; import com.google.tsunami.plugin.PluginType; import com.google.tsunami.plugin.VulnDetector; import com.google.tsunami.plugin.annotations.PluginInfo; import com.google.tsunami.proto.DetectionReport; import com.google.tsunami.proto.DetectionReportList; import com.google.tsunami.proto.DetectionStatus; import com.google.tsunami.proto.NetworkService; import com.google.tsunami.proto.Severity; import com.google.tsunami.proto.TargetInfo; import com.google.tsunami.proto.Vulnerability; import com.google.tsunami.proto.VulnerabilityId; /** A fake VulnDetector plugin for testing purpose only. */ @PluginInfo( type = PluginType.VULN_DETECTION, name = "FakeVulnDetector2", version = "v0.1", description = "Another fake VulnDetector.", author = "fake", bootstrapModule = FakeVulnDetectorBootstrapModule2.class) public class FakeVulnDetector2 implements VulnDetector { public static DetectionReport getFakeDetectionReport( TargetInfo targetInfo, NetworkService networkService) { return DetectionReport.newBuilder() .setTargetInfo(targetInfo) .setNetworkService(networkService) .setDetectionTimestamp(Timestamps.fromMillis(9876543210L)) .setDetectionStatus(DetectionStatus.VULNERABILITY_VERIFIED) .setVulnerability(getAdvisoriesStatic().get(0)) .build(); } @Override public ImmutableList getAdvisories() { return getAdvisoriesStatic(); } @Override public DetectionReportList detect( TargetInfo targetInfo, ImmutableList matchedServices) { return DetectionReportList.newBuilder() .addAllDetectionReports( matchedServices.stream() .map(networkService -> getFakeDetectionReport(targetInfo, networkService)) .collect(toImmutableList())) .build(); } private static ImmutableList getAdvisoriesStatic() { return ImmutableList.of( Vulnerability.newBuilder() .setMainId(VulnerabilityId.newBuilder().setPublisher("GOOGLE").setValue("FakeVuln2")) .setSeverity(Severity.MEDIUM) .setTitle("FakeTitle2") .setDescription("FakeDescription2") .build()); } } ================================================ FILE: plugin/src/main/java/com/google/tsunami/plugin/testing/FakeVulnDetectorBootstrapModule.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.plugin.testing; import com.google.tsunami.plugin.PluginBootstrapModule; /** Bootstrapping module for {@link FakeVulnDetector}. */ public class FakeVulnDetectorBootstrapModule extends PluginBootstrapModule { @Override protected void configurePlugin() { registerPlugin(FakeVulnDetector.class); } } ================================================ FILE: plugin/src/main/java/com/google/tsunami/plugin/testing/FakeVulnDetectorBootstrapModule2.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.plugin.testing; import com.google.tsunami.plugin.PluginBootstrapModule; /** Bootstrapping module for {@link FakeVulnDetector2}. */ public class FakeVulnDetectorBootstrapModule2 extends PluginBootstrapModule { @Override protected void configurePlugin() { registerPlugin(FakeVulnDetector2.class); } } ================================================ FILE: plugin/src/main/java/com/google/tsunami/plugin/testing/FakeVulnDetectorBootstrapModuleEmpty.java ================================================ /* * Copyright 2025 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.plugin.testing; import com.google.tsunami.plugin.PluginBootstrapModule; /** Bootstrapping module for {@link FakeVulnDetectorEmpty}. */ public class FakeVulnDetectorBootstrapModuleEmpty extends PluginBootstrapModule { @Override protected void configurePlugin() { registerPlugin(FakeVulnDetectorEmpty.class); } } ================================================ FILE: plugin/src/main/java/com/google/tsunami/plugin/testing/FakeVulnDetectorEmpty.java ================================================ /* * Copyright 2025 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.plugin.testing; import com.google.common.collect.ImmutableList; import com.google.tsunami.plugin.PluginType; import com.google.tsunami.plugin.VulnDetector; import com.google.tsunami.plugin.annotations.PluginInfo; import com.google.tsunami.proto.DetectionReportList; import com.google.tsunami.proto.NetworkService; import com.google.tsunami.proto.TargetInfo; import com.google.tsunami.proto.Vulnerability; /** A fake VulnDetector plugin for testing purpose only. */ @PluginInfo( type = PluginType.VULN_DETECTION, name = "FakeVulnDetectorEmpty", version = "v0.1", description = "Another fake VulnDetector.", author = "fake", bootstrapModule = FakeVulnDetectorBootstrapModuleEmpty.class) public class FakeVulnDetectorEmpty implements VulnDetector { @Override public DetectionReportList detect( TargetInfo targetInfo, ImmutableList matchedServices) { return DetectionReportList.newBuilder().build(); } @Override public ImmutableList getAdvisories() { return ImmutableList.of(); } } ================================================ FILE: plugin/src/main/resources/com/google/tsunami/plugin/payload/payload_definitions.yaml ================================================ # Copyright 2022 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # See payload_generator.proto for how this file is defined payloads: - name: general_ssrf_callback interpretation_environment: INTERPRETATION_ANY execution_environment: EXEC_ANY uses_callback_server: true payload_string: $TSUNAMI_PAYLOAD_TOKEN_URL vulnerability_type: - SSRF - name: general_ssrf_no_callback # Detectors could break if this page changes or goes down. interpretation_environment: INTERPRETATION_ANY execution_environment: EXEC_ANY uses_callback_server: false payload_string: http://public-firing-range.appspot.com/ validation_type: VALIDATION_REGEX validation_regex: (?s).*What is the Firing Range?.* vulnerability_type: - SSRF - name: linux_callback interpretation_environment: LINUX_SHELL execution_environment: EXEC_INTERPRETATION_ENVIRONMENT uses_callback_server: true payload_string: curl $TSUNAMI_PAYLOAD_TOKEN_URL vulnerability_type: - REFLECTIVE_RCE - BLIND_RCE - name: linux_root_crontab # Write the crontab payload to /etc/cron.d/tsunami_rce_cron interpretation_environment: LINUX_ROOT_CRONTAB execution_environment: EXEC_INTERPRETATION_ENVIRONMENT uses_callback_server: true payload_string: "* * * * * root curl $TSUNAMI_PAYLOAD_TOKEN_URL \n" vulnerability_type: - ARBITRARY_FILE_WRITE - name: linux_curl_trace_read interpretation_environment: LINUX_SHELL execution_environment: EXEC_INTERPRETATION_ENVIRONMENT uses_callback_server: false payload_string: curl --trace /tmp/tsunami-rce -- tsunami-rce-$TSUNAMI_PAYLOAD_TOKEN_RANDOM validation_type: VALIDATION_REGEX validation_regex: (?s).*tsunami-rce-$TSUNAMI_PAYLOAD_TOKEN_RANDOM.* vulnerability_type: - BLIND_RCE_FILE_READ - name: windows_callback interpretation_environment: WINDOWS_SHELL execution_environment: EXEC_INTERPRETATION_ENVIRONMENT uses_callback_server: true payload_string: powershell -Command "Invoke-WebRequest -URI $TSUNAMI_PAYLOAD_TOKEN_URL" vulnerability_type: - REFLECTIVE_RCE - BLIND_RCE - name: windows_echo interpretation_environment: WINDOWS_SHELL execution_environment: EXEC_INTERPRETATION_ENVIRONMENT uses_callback_server: false payload_string: powershell -Command "echo TSUNAMI_PAYLOAD_START$(echo $TSUNAMI_PAYLOAD_TOKEN_RANDOM)TSUNAMI_PAYLOAD_END" validation_type: VALIDATION_REGEX validation_regex: (?s).*TSUNAMI_PAYLOAD_START$TSUNAMI_PAYLOAD_TOKEN_RANDOMTSUNAMI_PAYLOAD_END.* vulnerability_type: - REFLECTIVE_RCE - name: linux_printf interpretation_environment: LINUX_SHELL execution_environment: EXEC_INTERPRETATION_ENVIRONMENT uses_callback_server: false payload_string: printf %s%s%s TSUNAMI_PAYLOAD_START $TSUNAMI_PAYLOAD_TOKEN_RANDOM TSUNAMI_PAYLOAD_END validation_type: VALIDATION_REGEX validation_regex: (?s).*TSUNAMI_PAYLOAD_START$TSUNAMI_PAYLOAD_TOKEN_RANDOMTSUNAMI_PAYLOAD_END.* vulnerability_type: - REFLECTIVE_RCE - name: java_string_format interpretationEnvironment: JAVA executionEnvironment: EXEC_INTERPRETATION_ENVIRONMENT usesCallbackServer: false payloadString: String.format("%s%s%s", "TSUNAMI_PAYLOAD_START", "$TSUNAMI_PAYLOAD_TOKEN_RANDOM", "TSUNAMI_PAYLOAD_END") validation_type: VALIDATION_REGEX validation_regex: (?s).*TSUNAMI_PAYLOAD_START$TSUNAMI_PAYLOAD_TOKEN_RANDOMTSUNAMI_PAYLOAD_END.* vulnerabilityType: - REFLECTIVE_RCE - name: jsp_print interpretation_environment: JSP execution_environment: EXEC_INTERPRETATION_ENVIRONMENT uses_callback_server: false payload_string: <% out.print(String.format("%s%s%s","TSUNAMI_PAYLOAD_START", "$TSUNAMI_PAYLOAD_TOKEN_RANDOM", "TSUNAMI_PAYLOAD_END")); %> validation_type: VALIDATION_REGEX validation_regex: (?s).*TSUNAMI_PAYLOAD_START$TSUNAMI_PAYLOAD_TOKEN_RANDOMTSUNAMI_PAYLOAD_END.* vulnerability_type: - REFLECTIVE_RCE ================================================ FILE: plugin/src/test/java/com/google/tsunami/plugin/PluginDefinitionTest.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.plugin; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; import com.google.tsunami.plugin.annotations.PluginInfo; import com.google.tsunami.plugin.testing.FakeRemoteVulnDetector; import com.google.tsunami.plugin.testing.FakeVulnDetector; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** Tests for {@link PluginDefinition}. */ @RunWith(JUnit4.class) public class PluginDefinitionTest { @Test public void id_always_generatesCorrectPluginId() { PluginDefinition pluginDefinition = PluginDefinition.forPlugin(FakeVulnDetector.class); assertThat(pluginDefinition.id()).isEqualTo("/fake/VULN_DETECTION/FakeVulnDetector/v0.1"); } @Test public void forPlugin_whenPluginHasNoAnnotation_throwsException() { assertThrows( IllegalStateException.class, () -> PluginDefinition.forPlugin(NoAnnotationPlugin.class)); } @Test public void forRemotePlugin_always_generatesCorrectDefinition() { PluginInfo pluginInfo = FakeRemoteVulnDetector.class.getAnnotation(PluginInfo.class); PluginDefinition pluginDefinition = PluginDefinition.forRemotePlugin(pluginInfo); assertThat(pluginDefinition.type()).isEqualTo(pluginInfo.type()); assertThat(pluginDefinition.name()).isEqualTo(pluginInfo.name()); assertThat(pluginDefinition.author()).isEqualTo(pluginInfo.author()); assertThat(pluginDefinition.version()).isEqualTo(pluginInfo.version()); assertThat(pluginDefinition.id()) .isEqualTo("/fake/REMOTE_VULN_DETECTION/FakeRemoteVulnDetector/v0.1"); } @Test public void forRemotePlugin_whenPassedNull_throwsException() { assertThrows(NullPointerException.class, () -> PluginDefinition.forRemotePlugin(null)); } private static final class NoAnnotationPlugin implements TsunamiPlugin {} } ================================================ FILE: plugin/src/test/java/com/google/tsunami/plugin/PluginExecutorImplTest.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.plugin; import static com.google.common.truth.Truth.assertThat; import com.google.common.base.Stopwatch; import com.google.common.testing.FakeTicker; import com.google.common.util.concurrent.ListeningScheduledExecutorService; import com.google.common.util.concurrent.MoreExecutors; import com.google.tsunami.plugin.PluginExecutor.PluginExecutorConfig; import com.google.tsunami.plugin.PluginManager.PluginMatchingResult; import com.google.tsunami.plugin.testing.FakePortScanner; import java.time.Duration; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executors; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** Tests for {@link PluginExecutorImpl}. */ @RunWith(JUnit4.class) public final class PluginExecutorImplTest { private static final PluginMatchingResult FAKE_MATCHING_RESULT = PluginMatchingResult.builder() .setTsunamiPlugin(new FakePortScanner()) .setPluginDefinition(PluginDefinition.forPlugin(FakePortScanner.class)) .build(); private static final Duration TICK_DURATION = Duration.ofSeconds(1); private static final ListeningScheduledExecutorService PLUGIN_EXECUTION_THREAD_POOL = MoreExecutors.listeningDecorator(Executors.newScheduledThreadPool(1)); private final FakeTicker ticker = new FakeTicker().setAutoIncrementStep(TICK_DURATION); private final Stopwatch executionStopWatch = Stopwatch.createUnstarted(ticker); @Test public void executeAsync_whenSucceeded_returnsSucceededResult() throws ExecutionException, InterruptedException { PluginExecutorConfig executorConfig = PluginExecutorConfig.builder() .setMatchedPlugin(FAKE_MATCHING_RESULT) .setPluginExecutionLogic(() -> "result data") .build(); PluginExecutionResult executionResult = new PluginExecutorImpl(PLUGIN_EXECUTION_THREAD_POOL, executionStopWatch) .executeAsync(executorConfig) .get(); assertThat(executionResult.exception()).isEmpty(); assertThat(executionResult.isSucceeded()).isTrue(); assertThat(executionResult.executionStopwatch().elapsed()).isEqualTo(TICK_DURATION); assertThat(executionResult.resultData()).hasValue("result data"); } @Test public void executeAsync_whenFailedWithPluginExecutionException_returnsFailedResult() throws ExecutionException, InterruptedException { PluginExecutorConfig executorConfig = PluginExecutorConfig.builder() .setMatchedPlugin(FAKE_MATCHING_RESULT) .setPluginExecutionLogic( () -> { throw new PluginExecutionException("test exception"); }) .build(); PluginExecutionResult executionResult = new PluginExecutorImpl(PLUGIN_EXECUTION_THREAD_POOL, executionStopWatch) .executeAsync(executorConfig) .get(); assertThat(executionResult.exception()).isPresent(); assertThat(executionResult.exception().get()).hasCauseThat().isNull(); assertThat(executionResult.exception().get()).hasMessageThat().contains("test exception"); assertThat(executionResult.isSucceeded()).isFalse(); assertThat(executionResult.executionStopwatch().elapsed()).isEqualTo(TICK_DURATION); assertThat(executionResult.resultData()).isEmpty(); } @Test public void executeAsync_whenFailedWithUnknownException_returnsFailedResult() throws ExecutionException, InterruptedException { PluginExecutorConfig executorConfig = PluginExecutorConfig.builder() .setMatchedPlugin(FAKE_MATCHING_RESULT) .setPluginExecutionLogic( () -> { throw new RuntimeException("test unknown exception"); }) .build(); PluginExecutionResult executionResult = new PluginExecutorImpl(PLUGIN_EXECUTION_THREAD_POOL, executionStopWatch) .executeAsync(executorConfig) .get(); assertThat(executionResult.exception()).isPresent(); assertThat(executionResult.exception().get()) .hasCauseThat() .isInstanceOf(RuntimeException.class); assertThat(executionResult.exception().get()) .hasMessageThat() .contains( String.format("Plugin execution error on '%s'.", FAKE_MATCHING_RESULT.pluginId())); assertThat(executionResult.isSucceeded()).isFalse(); assertThat(executionResult.executionStopwatch().elapsed()).isEqualTo(TICK_DURATION); assertThat(executionResult.resultData()).isEmpty(); } } ================================================ FILE: plugin/src/test/java/com/google/tsunami/plugin/PluginLoadingModuleTest.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.plugin; import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; import com.google.common.collect.ImmutableList; import com.google.inject.CreationException; import com.google.inject.Guice; import com.google.inject.Key; import com.google.inject.util.Types; import com.google.tsunami.plugin.annotations.PluginInfo; import com.google.tsunami.proto.DetectionReportList; import com.google.tsunami.proto.NetworkService; import com.google.tsunami.proto.PortScanningReport; import com.google.tsunami.proto.ScanTarget; import com.google.tsunami.proto.TargetInfo; import com.google.tsunami.proto.Vulnerability; import io.github.classgraph.ClassGraph; import io.github.classgraph.ScanResult; import java.util.Map; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** Tests for {@link PluginLoadingModule}. */ @RunWith(JUnit4.class) public final class PluginLoadingModuleTest { @SuppressWarnings("unchecked") private static final Key> PLUGIN_BINDING_KEY = (Key>) Key.get(Types.mapOf(PluginDefinition.class, TsunamiPlugin.class)); private static final ImmutableList COMMON_CLASSES_TO_LOAD = ImmutableList.of( TsunamiPlugin.class.getTypeName(), PortScanner.class.getTypeName(), VulnDetector.class.getTypeName(), PluginBootstrapModule.class.getTypeName()); @Test public void configure_always_loadsAllTsunamiPlugins() { ImmutableList whitelistedClasses = ImmutableList.builder() .addAll(COMMON_CLASSES_TO_LOAD) .add( FakePortScanner.class.getTypeName(), FakePortScannerBootstrapModule.class.getTypeName(), FakeVulnDetector.class.getTypeName(), FakeVulnDetectorBootstrapModule.class.getTypeName(), FakeVulnDetector2.class.getTypeName(), FakeVulnDetector2.FakeVulnDetector2BootstrapModule.class.getTypeName()) .build(); try (ScanResult classScanResult = new ClassGraph() .enableAllInfo() .whitelistClasses(whitelistedClasses.toArray(new String[0])) .scan()) { Iterable> installedPluginTypes = Guice.createInjector(new PluginLoadingModule(true, classScanResult)) .getInstance(PLUGIN_BINDING_KEY) .values() .stream() .map(TsunamiPlugin::getClass) .collect(toImmutableList()); assertThat(installedPluginTypes) .containsExactly(FakePortScanner.class, FakeVulnDetector.class, FakeVulnDetector2.class); } } @Test public void configure_whenPluginMissingRequiredAnnotation_throwsException() { ImmutableList whitelistedClasses = ImmutableList.builder() .addAll(COMMON_CLASSES_TO_LOAD) .add(NoAnnotationDetector.class.getTypeName()) .build(); try (ScanResult classScanResult = new ClassGraph() .enableAllInfo() .whitelistClasses(whitelistedClasses.toArray(new String[0])) .scan()) { CreationException ex = assertThrows( CreationException.class, () -> Guice.createInjector(new PluginLoadingModule(true, classScanResult)) .getAllBindings()); assertThat(ex).hasCauseThat().isInstanceOf(IllegalStateException.class); } } @Test public void configure_whenPluginBootstrapModuleCannotBeInitialized_throwsException() { ImmutableList whitelistedClasses = ImmutableList.builder() .addAll(COMMON_CLASSES_TO_LOAD) .add( FakePortScanner.class.getTypeName(), FakePortScannerBootstrapModule.class.getTypeName()) .build(); try (ScanResult classScanResult = new ClassGraph() .enableAllInfo() .whitelistClasses(whitelistedClasses.toArray(new String[0])) .scan()) { assertThrows( AssertionError.class, () -> Guice.createInjector(new PluginLoadingModule(false, classScanResult)) .getAllBindings()); } } @PluginInfo( type = PluginType.PORT_SCAN, name = "FakePortScanner", version = "0.1", description = "FakePortScanner", author = "TestAuthor", bootstrapModule = FakePortScannerBootstrapModule.class) private static final class FakePortScanner implements PortScanner { @Override public PortScanningReport scan(ScanTarget scanTarget) { return null; } } private static final class FakePortScannerBootstrapModule extends PluginBootstrapModule { @Override protected void configurePlugin() { registerPlugin(FakePortScanner.class); } } @PluginInfo( type = PluginType.VULN_DETECTION, name = "FakeVulnDetector", version = "0.1", description = "FakeVulnDetector", author = "TestAuthor", bootstrapModule = FakeVulnDetectorBootstrapModule.class) private static final class FakeVulnDetector implements VulnDetector { @Override public DetectionReportList detect( TargetInfo targetInfo, ImmutableList matchedServices) { return null; } @Override public ImmutableList getAdvisories() { return ImmutableList.of(); } } private static final class FakeVulnDetectorBootstrapModule extends PluginBootstrapModule { @Override protected void configurePlugin() { registerPlugin(FakeVulnDetector.class); } } @PluginInfo( type = PluginType.VULN_DETECTION, name = "FakeVulnDetector2", version = "0.1", description = "FakeVulnDetector2", author = "TestAuthor", bootstrapModule = FakeVulnDetector2.FakeVulnDetector2BootstrapModule.class) private static final class FakeVulnDetector2 implements VulnDetector { @Override public DetectionReportList detect( TargetInfo targetInfo, ImmutableList matchedServices) { return null; } @Override public ImmutableList getAdvisories() { return ImmutableList.of(); } static final class FakeVulnDetector2BootstrapModule extends PluginBootstrapModule { @Override protected void configurePlugin() { registerPlugin(FakeVulnDetector2.class); } } } private static final class NoAnnotationDetector implements VulnDetector { @Override public DetectionReportList detect( TargetInfo targetInfo, ImmutableList matchedServices) { return null; } @Override public ImmutableList getAdvisories() { return ImmutableList.of(); } } } ================================================ FILE: plugin/src/test/java/com/google/tsunami/plugin/PluginManagerTest.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.plugin; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import com.google.inject.AbstractModule; import com.google.inject.Guice; import com.google.inject.multibindings.MapBinder; import com.google.tsunami.common.cli.CliOptionsModule; import com.google.tsunami.common.data.NetworkEndpointUtils; import com.google.tsunami.common.net.http.HttpClientModule; import com.google.tsunami.plugin.PluginManager.PluginMatchingResult; import com.google.tsunami.plugin.annotations.ForOperatingSystemClass; import com.google.tsunami.plugin.annotations.ForServiceName; import com.google.tsunami.plugin.annotations.ForSoftware; import com.google.tsunami.plugin.annotations.ForWebService; import com.google.tsunami.plugin.annotations.PluginInfo; import com.google.tsunami.plugin.payload.PayloadGeneratorModule; import com.google.tsunami.plugin.testing.FakePortScanner; import com.google.tsunami.plugin.testing.FakePortScanner2; import com.google.tsunami.plugin.testing.FakePortScannerBootstrapModule; import com.google.tsunami.plugin.testing.FakePortScannerBootstrapModule2; import com.google.tsunami.plugin.testing.FakeRemoteVulnDetector; import com.google.tsunami.plugin.testing.FakeServiceFingerprinterBootstrapModule; import com.google.tsunami.plugin.testing.FakeVulnDetector; import com.google.tsunami.plugin.testing.FakeVulnDetector2; import com.google.tsunami.plugin.testing.FakeVulnDetectorBootstrapModule; import com.google.tsunami.plugin.testing.FakeVulnDetectorBootstrapModule2; import com.google.tsunami.proto.DetectionReportList; import com.google.tsunami.proto.FingerprintingReport; import com.google.tsunami.proto.MatchedPlugin; import com.google.tsunami.proto.NetworkService; import com.google.tsunami.proto.OperatingSystemClass; import com.google.tsunami.proto.ReconnaissanceReport; import com.google.tsunami.proto.Software; import com.google.tsunami.proto.TargetInfo; import com.google.tsunami.proto.TargetOperatingSystemClass; import com.google.tsunami.proto.TargetServiceName; import com.google.tsunami.proto.TargetSoftware; import com.google.tsunami.proto.TransportProtocol; import com.google.tsunami.proto.Vulnerability; import io.github.classgraph.ClassGraph; import io.github.classgraph.ScanResult; import java.security.SecureRandom; import java.util.List; import java.util.Optional; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** Tests for {@link PluginManager}. */ @RunWith(JUnit4.class) public class PluginManagerTest { @Test public void getPortScanners_whenMultiplePortScannersInstalled_returnsAllPortScanners() { PluginManager pluginManager = Guice.createInjector( new HttpClientModule.Builder().build(), new PayloadGeneratorModule(new SecureRandom()), new FakePortScannerBootstrapModule(), new FakePortScannerBootstrapModule2(), new FakeServiceFingerprinterBootstrapModule(), new FakeVulnDetectorBootstrapModule()) .getInstance(PluginManager.class); ImmutableList> portScanners = pluginManager.getPortScanners(); assertThat( portScanners.stream() .map(pluginMatchingResult -> pluginMatchingResult.tsunamiPlugin().getClass())) .containsExactly(FakePortScanner.class, FakePortScanner2.class); } @Test public void getPortScanners_whenNoPortScannersInstalled_returnsEmptyList() { PluginManager pluginManager = Guice.createInjector( new HttpClientModule.Builder().build(), new PayloadGeneratorModule(new SecureRandom()), new FakeServiceFingerprinterBootstrapModule(), new FakeVulnDetectorBootstrapModule()) .getInstance(PluginManager.class); assertThat(pluginManager.getPortScanners()).isEmpty(); } @Test public void getPortScanner_whenMultiplePortScannersInstalled_returnsTheFirstMatchedPortScanner() { PluginManager pluginManager = Guice.createInjector( new HttpClientModule.Builder().build(), new PayloadGeneratorModule(new SecureRandom()), new FakePortScannerBootstrapModule(), new FakePortScannerBootstrapModule2(), new FakeServiceFingerprinterBootstrapModule(), new FakeVulnDetectorBootstrapModule()) .getInstance(PluginManager.class); ImmutableList> allPortScanners = pluginManager.getPortScanners(); Optional> firstMatchedPortScanner = pluginManager.getPortScanner(); assertThat(firstMatchedPortScanner).isPresent(); assertThat(firstMatchedPortScanner.get().pluginDefinition()) .isEqualTo(allPortScanners.get(0).pluginDefinition()); assertThat(firstMatchedPortScanner.get().tsunamiPlugin().getClass()) .isEqualTo(allPortScanners.get(0).tsunamiPlugin().getClass()); } @Test public void getPortScanner_whenNoPortScannersInstalled_returnsEmptyOptional() { PluginManager pluginManager = Guice.createInjector( new HttpClientModule.Builder().build(), new PayloadGeneratorModule(new SecureRandom()), new FakeServiceFingerprinterBootstrapModule(), new FakeVulnDetectorBootstrapModule()) .getInstance(PluginManager.class); assertThat(pluginManager.getPortScanner()).isEmpty(); } @Test public void getServiceFingerprinter_whenFingerprinterNotAnnotated_returnsEmpty() { NetworkService httpService = NetworkService.newBuilder() .setNetworkEndpoint(NetworkEndpointUtils.forIpAndPort("1.1.1.1", 80)) .setTransportProtocol(TransportProtocol.TCP) .setServiceName("http") .build(); PluginManager pluginManager = Guice.createInjector( new HttpClientModule.Builder().build(), new PayloadGeneratorModule(new SecureRandom()), new FakePortScannerBootstrapModule(), NoAnnotationFingerprinter.getModule()) .getInstance(PluginManager.class); Optional> fingerprinter = pluginManager.getServiceFingerprinter(httpService); assertThat(fingerprinter).isEmpty(); } @Test public void getServiceFingerprinter_whenFingerprinterHasMatch_returnsMatch() { NetworkService httpService = NetworkService.newBuilder() .setNetworkEndpoint(NetworkEndpointUtils.forIpAndPort("1.1.1.1", 80)) .setTransportProtocol(TransportProtocol.TCP) .setServiceName("http") .build(); PluginManager pluginManager = Guice.createInjector( new HttpClientModule.Builder().build(), new PayloadGeneratorModule(new SecureRandom()), new FakePortScannerBootstrapModule(), new FakeServiceFingerprinterBootstrapModule()) .getInstance(PluginManager.class); Optional> fingerprinter = pluginManager.getServiceFingerprinter(httpService); assertThat(fingerprinter).isPresent(); assertThat(fingerprinter.get().matchedServices()).containsExactly(httpService); } @Test public void getServiceFingerprinter_whenNoFingerprinterMatches_returnsEmpty() { NetworkService httpsService = NetworkService.newBuilder() .setNetworkEndpoint(NetworkEndpointUtils.forIpAndPort("1.1.1.1", 80)) .setTransportProtocol(TransportProtocol.TCP) .setServiceName("https") .build(); PluginManager pluginManager = Guice.createInjector( new HttpClientModule.Builder().build(), new PayloadGeneratorModule(new SecureRandom()), new FakePortScannerBootstrapModule(), new FakeServiceFingerprinterBootstrapModule()) .getInstance(PluginManager.class); Optional> fingerprinter = pluginManager.getServiceFingerprinter(httpsService); assertThat(fingerprinter).isEmpty(); } @Test public void getServiceFingerprinter_whenForWebServiceAnnotationAndWebService_returnsMatch() { NetworkService httpProxyService = NetworkService.newBuilder() .setNetworkEndpoint(NetworkEndpointUtils.forIpAndPort("1.1.1.1", 80)) .setTransportProtocol(TransportProtocol.TCP) .setServiceName("http-proxy") .build(); NetworkService httpsService = NetworkService.newBuilder() .setNetworkEndpoint(NetworkEndpointUtils.forIpAndPort("1.1.1.1", 80)) .setTransportProtocol(TransportProtocol.TCP) .setServiceName("https") .build(); PluginManager pluginManager = Guice.createInjector( new HttpClientModule.Builder().build(), new PayloadGeneratorModule(new SecureRandom()), new FakePortScannerBootstrapModule(), FakeWebFingerprinter.getModule()) .getInstance(PluginManager.class); Optional> fingerprinter = pluginManager.getServiceFingerprinter(httpsService); assertThat(fingerprinter).isPresent(); assertThat(fingerprinter.get().matchedServices()).containsExactly(httpsService); fingerprinter = pluginManager.getServiceFingerprinter(httpProxyService); assertThat(fingerprinter).isPresent(); assertThat(fingerprinter.get().matchedServices()).containsExactly(httpProxyService); } @Test public void getServiceFingerprinter_whenForWebServiceAnnotationAndNonWebService_returnsEmpty() { NetworkService sshService = NetworkService.newBuilder() .setNetworkEndpoint(NetworkEndpointUtils.forIpAndPort("1.1.1.1", 80)) .setTransportProtocol(TransportProtocol.TCP) .setServiceName("ssh") .build(); NetworkService rdpService = NetworkService.newBuilder() .setNetworkEndpoint(NetworkEndpointUtils.forIpAndPort("1.1.1.1", 80)) .setTransportProtocol(TransportProtocol.TCP) .setServiceName("rdp") .build(); PluginManager pluginManager = Guice.createInjector( new HttpClientModule.Builder().build(), new PayloadGeneratorModule(new SecureRandom()), new FakePortScannerBootstrapModule(), FakeWebFingerprinter.getModule()) .getInstance(PluginManager.class); assertThat(pluginManager.getServiceFingerprinter(sshService)).isEmpty(); assertThat(pluginManager.getServiceFingerprinter(rdpService)).isEmpty(); } @Test public void getVulnDetectors_whenMultipleVulnDetectorsInstalledNoFiltering_returnsAllVulnDetector() { NetworkService fakeNetworkService1 = NetworkService.newBuilder() .setNetworkEndpoint(NetworkEndpointUtils.forIpAndPort("1.1.1.1", 80)) .setTransportProtocol(TransportProtocol.TCP) .setServiceName("http") .build(); NetworkService fakeNetworkService2 = NetworkService.newBuilder() .setNetworkEndpoint(NetworkEndpointUtils.forIpAndPort("1.1.1.1", 443)) .setTransportProtocol(TransportProtocol.TCP) .setServiceName("https") .build(); ReconnaissanceReport fakeReconnaissanceReport = ReconnaissanceReport.newBuilder() .setTargetInfo(TargetInfo.getDefaultInstance()) .addNetworkServices(fakeNetworkService1) .addNetworkServices(fakeNetworkService2) .build(); PluginManager pluginManager = Guice.createInjector( new HttpClientModule.Builder().build(), new PayloadGeneratorModule(new SecureRandom()), new FakePortScannerBootstrapModule(), new FakeServiceFingerprinterBootstrapModule(), new FakeVulnDetectorBootstrapModule(), new FakeVulnDetectorBootstrapModule2()) .getInstance(PluginManager.class); ImmutableList> vulnDetectors = pluginManager.getVulnDetectors(fakeReconnaissanceReport); assertThat( vulnDetectors.stream() .map(pluginMatchingResult -> pluginMatchingResult.tsunamiPlugin().getClass())) .containsExactly(FakeVulnDetector.class, FakeVulnDetector2.class); assertThat(vulnDetectors.stream().map(PluginMatchingResult::matchedServices)) .containsExactly( fakeReconnaissanceReport.getNetworkServicesList(), fakeReconnaissanceReport.getNetworkServicesList()); } @Test public void getVulnDetectors_whenServiceNameFilterHasMatchingService_returnsMatchedService() { NetworkService httpService = NetworkService.newBuilder() .setNetworkEndpoint(NetworkEndpointUtils.forIpAndPort("1.1.1.1", 80)) .setTransportProtocol(TransportProtocol.TCP) .setServiceName("http") .build(); NetworkService httpsService = NetworkService.newBuilder() .setNetworkEndpoint(NetworkEndpointUtils.forIpAndPort("1.1.1.1", 443)) .setTransportProtocol(TransportProtocol.TCP) .setServiceName("https") .build(); NetworkService noNameService = NetworkService.newBuilder() .setNetworkEndpoint(NetworkEndpointUtils.forIpAndPort("1.1.1.1", 12345)) .setTransportProtocol(TransportProtocol.TCP) .build(); ReconnaissanceReport fakeReconnaissanceReport = ReconnaissanceReport.newBuilder() .setTargetInfo(TargetInfo.getDefaultInstance()) .addNetworkServices(httpService) .addNetworkServices(httpsService) .addNetworkServices(noNameService) .build(); PluginManager pluginManager = Guice.createInjector( new HttpClientModule.Builder().build(), new PayloadGeneratorModule(new SecureRandom()), new FakePortScannerBootstrapModule(), new FakeServiceFingerprinterBootstrapModule(), FakeServiceNameFilteringDetector.getModule()) .getInstance(PluginManager.class); ImmutableList> vulnDetectors = pluginManager.getVulnDetectors(fakeReconnaissanceReport); assertThat(vulnDetectors).hasSize(1); assertThat(vulnDetectors.get(0).tsunamiPlugin().getClass()) .isEqualTo(FakeServiceNameFilteringDetector.class); assertThat(vulnDetectors.get(0).matchedServices()).containsExactly(httpService, noNameService); } @Test public void getVulnDetectors_whenServiceNameFilterHasNoMatchingService_returnsEmpty() { NetworkService httpsService = NetworkService.newBuilder() .setNetworkEndpoint(NetworkEndpointUtils.forIpAndPort("1.1.1.1", 443)) .setTransportProtocol(TransportProtocol.TCP) .setServiceName("https") .build(); ReconnaissanceReport fakeReconnaissanceReport = ReconnaissanceReport.newBuilder() .setTargetInfo(TargetInfo.getDefaultInstance()) .addNetworkServices(httpsService) .build(); PluginManager pluginManager = Guice.createInjector( new HttpClientModule.Builder().build(), new PayloadGeneratorModule(new SecureRandom()), new FakePortScannerBootstrapModule(), new FakeServiceFingerprinterBootstrapModule(), FakeServiceNameFilteringDetector.getModule()) .getInstance(PluginManager.class); ImmutableList> vulnDetectors = pluginManager.getVulnDetectors(fakeReconnaissanceReport); assertThat(vulnDetectors).isEmpty(); } @Test public void getVulnDetectors_whenSoftwareFilterHasMatchingService_returnsMatchedService() { NetworkService wordPressService = NetworkService.newBuilder() .setNetworkEndpoint(NetworkEndpointUtils.forIpAndPort("1.1.1.1", 80)) .setTransportProtocol(TransportProtocol.TCP) .setServiceName("http") .setSoftware(Software.newBuilder().setName("WordPress")) .build(); NetworkService jenkinsService = NetworkService.newBuilder() .setNetworkEndpoint(NetworkEndpointUtils.forIpAndPort("1.1.1.1", 443)) .setTransportProtocol(TransportProtocol.TCP) .setServiceName("https") .setSoftware(Software.newBuilder().setName("Jenkins")) .build(); NetworkService noNameService = NetworkService.newBuilder() .setNetworkEndpoint(NetworkEndpointUtils.forIpAndPort("1.1.1.1", 12345)) .setTransportProtocol(TransportProtocol.TCP) .build(); ReconnaissanceReport fakeReconnaissanceReport = ReconnaissanceReport.newBuilder() .setTargetInfo(TargetInfo.getDefaultInstance()) .addNetworkServices(wordPressService) .addNetworkServices(jenkinsService) .addNetworkServices(noNameService) .build(); PluginManager pluginManager = Guice.createInjector( new HttpClientModule.Builder().build(), new PayloadGeneratorModule(new SecureRandom()), new FakePortScannerBootstrapModule(), new FakeServiceFingerprinterBootstrapModule(), FakeSoftwareFilteringDetector.getModule()) .getInstance(PluginManager.class); ImmutableList> vulnDetectors = pluginManager.getVulnDetectors(fakeReconnaissanceReport); assertThat(vulnDetectors).hasSize(1); assertThat(vulnDetectors.get(0).tsunamiPlugin().getClass()) .isEqualTo(FakeSoftwareFilteringDetector.class); assertThat(vulnDetectors.get(0).matchedServices()) .containsExactly(jenkinsService, noNameService); } @Test public void getVulnDetectors_whenOsFilterHasNoMatchingClass_returnsEmpty() { NetworkService wordPressService = NetworkService.newBuilder() .setNetworkEndpoint(NetworkEndpointUtils.forIpAndPort("1.1.1.1", 80)) .setTransportProtocol(TransportProtocol.TCP) .setServiceName("http") .setSoftware(Software.newBuilder().setName("WordPress")) .build(); NetworkService jenkinsService = NetworkService.newBuilder() .setNetworkEndpoint(NetworkEndpointUtils.forIpAndPort("1.1.1.1", 443)) .setTransportProtocol(TransportProtocol.TCP) .setServiceName("https") .setSoftware(Software.newBuilder().setName("Jenkins")) .build(); NetworkService noNameService = NetworkService.newBuilder() .setNetworkEndpoint(NetworkEndpointUtils.forIpAndPort("1.1.1.1", 12345)) .setTransportProtocol(TransportProtocol.TCP) .build(); ReconnaissanceReport fakeReconnaissanceReport = ReconnaissanceReport.newBuilder() .setTargetInfo(TargetInfo.getDefaultInstance()) .addNetworkServices(wordPressService) .addNetworkServices(jenkinsService) .addNetworkServices(noNameService) .build(); PluginManager pluginManager = Guice.createInjector( new HttpClientModule.Builder().build(), new PayloadGeneratorModule(new SecureRandom()), new FakePortScannerBootstrapModule(), new FakeServiceFingerprinterBootstrapModule(), FakeOsFilteringDetector.getModule()) .getInstance(PluginManager.class); ImmutableList> vulnDetectors = pluginManager.getVulnDetectors(fakeReconnaissanceReport); // matchVulnDetectors returns Optional.empty() when there is no match. assertThat(vulnDetectors).isEmpty(); } @Test public void getVulnDetectors_whenOsFilterHasMatchingClass_returnsMatches() { NetworkService wordPressService = NetworkService.newBuilder() .setNetworkEndpoint(NetworkEndpointUtils.forIpAndPort("1.1.1.1", 80)) .setTransportProtocol(TransportProtocol.TCP) .setServiceName("http") .setSoftware(Software.newBuilder().setName("WordPress")) .build(); NetworkService jenkinsService = NetworkService.newBuilder() .setNetworkEndpoint(NetworkEndpointUtils.forIpAndPort("1.1.1.1", 443)) .setTransportProtocol(TransportProtocol.TCP) .setServiceName("https") .setSoftware(Software.newBuilder().setName("Jenkins")) .build(); NetworkService noNameService = NetworkService.newBuilder() .setNetworkEndpoint(NetworkEndpointUtils.forIpAndPort("1.1.1.1", 12345)) .setTransportProtocol(TransportProtocol.TCP) .build(); ReconnaissanceReport fakeReconnaissanceReport = ReconnaissanceReport.newBuilder() .setTargetInfo( TargetInfo.newBuilder() .addOperatingSystemClasses( OperatingSystemClass.newBuilder() .setVendor("Vendor") .setOsFamily("FakeOS") .setAccuracy(99))) .addNetworkServices(wordPressService) .addNetworkServices(jenkinsService) .addNetworkServices(noNameService) .build(); PluginManager pluginManager = Guice.createInjector( new HttpClientModule.Builder().build(), new PayloadGeneratorModule(new SecureRandom()), new FakePortScannerBootstrapModule(), new FakeServiceFingerprinterBootstrapModule(), FakeOsFilteringDetector.getModule()) .getInstance(PluginManager.class); ImmutableList> vulnDetectors = pluginManager.getVulnDetectors(fakeReconnaissanceReport); assertThat(vulnDetectors).hasSize(1); assertThat(vulnDetectors.get(0).tsunamiPlugin().getClass()) .isEqualTo(FakeOsFilteringDetector.class); // And matches everything (as all these services are running on the same target) assertThat(vulnDetectors.get(0).matchedServices()) .containsExactly(wordPressService, jenkinsService, noNameService); } @Test public void getVulnDetectors_whenOsServiceFilterHasMatchingClass_returnsMatches() { NetworkService wordPressService = NetworkService.newBuilder() .setNetworkEndpoint(NetworkEndpointUtils.forIpAndPort("1.1.1.1", 80)) .setTransportProtocol(TransportProtocol.TCP) .setServiceName("http") .setSoftware(Software.newBuilder().setName("WordPress")) .build(); NetworkService jenkinsService = NetworkService.newBuilder() .setNetworkEndpoint(NetworkEndpointUtils.forIpAndPort("1.1.1.1", 443)) .setTransportProtocol(TransportProtocol.TCP) .setServiceName("https") .setSoftware(Software.newBuilder().setName("Jenkins")) .build(); NetworkService noNameService = NetworkService.newBuilder() .setNetworkEndpoint(NetworkEndpointUtils.forIpAndPort("1.1.1.1", 12345)) .setTransportProtocol(TransportProtocol.TCP) .build(); ReconnaissanceReport fakeReconnaissanceReport = ReconnaissanceReport.newBuilder() .setTargetInfo( TargetInfo.newBuilder() .addOperatingSystemClasses( OperatingSystemClass.newBuilder() .setVendor("Vendor") .setOsFamily("FakeOS") .setAccuracy(99))) .addNetworkServices(wordPressService) .addNetworkServices(jenkinsService) .addNetworkServices(noNameService) .build(); PluginManager pluginManager = Guice.createInjector( new HttpClientModule.Builder().build(), new PayloadGeneratorModule(new SecureRandom()), new FakePortScannerBootstrapModule(), new FakeServiceFingerprinterBootstrapModule(), FakeOsServiceFilteringDetector.getModule()) .getInstance(PluginManager.class); ImmutableList> vulnDetectors = pluginManager.getVulnDetectors(fakeReconnaissanceReport); assertThat(vulnDetectors).hasSize(1); assertThat(vulnDetectors.get(0).tsunamiPlugin().getClass()) .isEqualTo(FakeOsServiceFilteringDetector.class); // And matches the ones with the Jenkins software (and noname as well, as no software info is // present there; see hasMatchingServiceName) assertThat(vulnDetectors.get(0).matchedServices()) .containsExactly(jenkinsService, noNameService); } @Test public void getVulnDetectors_whenSoftwareFilterHasNoMatchingService_returnsEmpty() { NetworkService wordPressService = NetworkService.newBuilder() .setNetworkEndpoint(NetworkEndpointUtils.forIpAndPort("1.1.1.1", 443)) .setTransportProtocol(TransportProtocol.TCP) .setServiceName("https") .setSoftware(Software.newBuilder().setName("WordPress")) .build(); ReconnaissanceReport fakeReconnaissanceReport = ReconnaissanceReport.newBuilder() .setTargetInfo(TargetInfo.getDefaultInstance()) .addNetworkServices(wordPressService) .build(); PluginManager pluginManager = Guice.createInjector( new HttpClientModule.Builder().build(), new PayloadGeneratorModule(new SecureRandom()), new FakePortScannerBootstrapModule(), new FakeServiceFingerprinterBootstrapModule(), FakeSoftwareFilteringDetector.getModule()) .getInstance(PluginManager.class); ImmutableList> vulnDetectors = pluginManager.getVulnDetectors(fakeReconnaissanceReport); assertThat(vulnDetectors).isEmpty(); } @Test public void getVulnDetectors_whenNoVulnDetectorsInstalled_returnsEmptyList() { NetworkService fakeNetworkService1 = NetworkService.newBuilder() .setNetworkEndpoint(NetworkEndpointUtils.forIpAndPort("1.1.1.1", 80)) .setTransportProtocol(TransportProtocol.TCP) .setServiceName("http") .build(); NetworkService fakeNetworkService2 = NetworkService.newBuilder() .setNetworkEndpoint(NetworkEndpointUtils.forIpAndPort("1.1.1.1", 443)) .setTransportProtocol(TransportProtocol.TCP) .setServiceName("https") .build(); ReconnaissanceReport fakeReconnaissanceReport = ReconnaissanceReport.newBuilder() .setTargetInfo(TargetInfo.getDefaultInstance()) .addNetworkServices(fakeNetworkService1) .addNetworkServices(fakeNetworkService2) .build(); PluginManager pluginManager = Guice.createInjector( new HttpClientModule.Builder().build(), new PayloadGeneratorModule(new SecureRandom()), new FakePortScannerBootstrapModule(), new FakeServiceFingerprinterBootstrapModule()) .getInstance(PluginManager.class); assertThat(pluginManager.getVulnDetectors(fakeReconnaissanceReport)).isEmpty(); } @Test public void getVulnDetectors_whenRemotePluginsInstalledNoFiltering_returnsAllRemoteTsunamiPlugins() throws Exception { NetworkService fakeNetworkService1 = NetworkService.newBuilder() .setNetworkEndpoint(NetworkEndpointUtils.forIpAndPort("1.1.1.1", 80)) .setTransportProtocol(TransportProtocol.TCP) .setServiceName("http") .build(); NetworkService fakeNetworkService2 = NetworkService.newBuilder() .setNetworkEndpoint(NetworkEndpointUtils.forIpAndPort("1.1.1.1", 443)) .setTransportProtocol(TransportProtocol.TCP) .setServiceName("https") .build(); ReconnaissanceReport fakeReconnaissanceReport = ReconnaissanceReport.newBuilder() .setTargetInfo(TargetInfo.getDefaultInstance()) .addNetworkServices(fakeNetworkService1) .addNetworkServices(fakeNetworkService2) .build(); PluginManager pluginManager = Guice.createInjector( new HttpClientModule.Builder().build(), new PayloadGeneratorModule(new SecureRandom()), new FakeServiceFingerprinterBootstrapModule(), new FakeRemoteVulnDetectorLoadingModule(2)) .getInstance(PluginManager.class); ImmutableList> remotePlugins = pluginManager.getVulnDetectors(fakeReconnaissanceReport); assertThat( remotePlugins.stream() .map(pluginMatchingResult -> pluginMatchingResult.tsunamiPlugin().getClass())) .containsExactly(FakeRemoteVulnDetector.class, FakeRemoteVulnDetector.class); } @Test public void getVulnDetectors_whenRemoteDetectorServiceNameFilterHasMatchingService_returnsMatchedService() { NetworkService httpService = NetworkService.newBuilder() .setNetworkEndpoint(NetworkEndpointUtils.forIpAndPort("1.1.1.1", 80)) .setTransportProtocol(TransportProtocol.TCP) .setServiceName("http") .build(); NetworkService httpsService = NetworkService.newBuilder() .setNetworkEndpoint(NetworkEndpointUtils.forIpAndPort("1.1.1.1", 443)) .setTransportProtocol(TransportProtocol.TCP) .setServiceName("https") .build(); NetworkService noNameService = NetworkService.newBuilder() .setNetworkEndpoint(NetworkEndpointUtils.forIpAndPort("1.1.1.1", 12345)) .setTransportProtocol(TransportProtocol.TCP) .build(); ReconnaissanceReport fakeReconnaissanceReport = ReconnaissanceReport.newBuilder() .setTargetInfo(TargetInfo.getDefaultInstance()) .addNetworkServices(httpService) .addNetworkServices(httpsService) .addNetworkServices(noNameService) .build(); PluginManager pluginManager = Guice.createInjector( new HttpClientModule.Builder().build(), new PayloadGeneratorModule(new SecureRandom()), new FakePortScannerBootstrapModule(), new FakeServiceFingerprinterBootstrapModule(), FakeFilteringRemoteDetector.getModule()) .getInstance(PluginManager.class); ImmutableList> vulnDetectors = pluginManager.getVulnDetectors(fakeReconnaissanceReport); assertThat(vulnDetectors).hasSize(1); ImmutableList matchedResult = ((FakeFilteringRemoteDetector) vulnDetectors.get(0).tsunamiPlugin()).getMatchedPlugins(); assertThat(matchedResult).isNotEmpty(); assertThat(matchedResult.get(0).getPlugin()) .isEqualTo(FakeFilteringRemoteDetector.getHttpServiceDefinition()); assertThat(matchedResult.get(0).getServicesList()).containsExactly(httpService, noNameService); } @Test public void getVulnDetectors_whenRemoteDetectorWithServiceNameHasNoMatch_returnsNoServices() { NetworkService httpsService = NetworkService.newBuilder() .setNetworkEndpoint(NetworkEndpointUtils.forIpAndPort("1.1.1.1", 443)) .setTransportProtocol(TransportProtocol.TCP) .setServiceName("https") .build(); ReconnaissanceReport fakeReconnaissanceReport = ReconnaissanceReport.newBuilder() .setTargetInfo(TargetInfo.getDefaultInstance()) .addNetworkServices(httpsService) .build(); PluginManager pluginManager = Guice.createInjector( new HttpClientModule.Builder().build(), new PayloadGeneratorModule(new SecureRandom()), new FakePortScannerBootstrapModule(), new FakeServiceFingerprinterBootstrapModule(), FakeFilteringRemoteDetector.getModule()) .getInstance(PluginManager.class); ImmutableList> vulnDetectors = pluginManager.getVulnDetectors(fakeReconnaissanceReport); assertThat(vulnDetectors).hasSize(1); ImmutableList matchedResult = ((FakeFilteringRemoteDetector) vulnDetectors.get(0).tsunamiPlugin()).getMatchedPlugins(); assertThat(matchedResult.get(0).getServicesList()).isEmpty(); } @Test public void getVulnDetectors_whenRemoteDetectorSoftwareFilterHasMatchingService_returnsMatchedService() { NetworkService wordPressService = NetworkService.newBuilder() .setNetworkEndpoint(NetworkEndpointUtils.forIpAndPort("1.1.1.1", 80)) .setTransportProtocol(TransportProtocol.TCP) .setServiceName("http") .setSoftware(Software.newBuilder().setName("WordPress")) .build(); NetworkService jenkinsService = NetworkService.newBuilder() .setNetworkEndpoint(NetworkEndpointUtils.forIpAndPort("1.1.1.1", 443)) .setTransportProtocol(TransportProtocol.TCP) .setServiceName("https") .setSoftware(Software.newBuilder().setName("Jenkins")) .build(); NetworkService noNameService = NetworkService.newBuilder() .setNetworkEndpoint(NetworkEndpointUtils.forIpAndPort("1.1.1.1", 12345)) .setTransportProtocol(TransportProtocol.TCP) .build(); ReconnaissanceReport fakeReconnaissanceReport = ReconnaissanceReport.newBuilder() .setTargetInfo(TargetInfo.getDefaultInstance()) .addNetworkServices(wordPressService) .addNetworkServices(jenkinsService) .addNetworkServices(noNameService) .build(); PluginManager pluginManager = Guice.createInjector( new HttpClientModule.Builder().build(), new PayloadGeneratorModule(new SecureRandom()), new FakePortScannerBootstrapModule(), new FakeServiceFingerprinterBootstrapModule(), FakeFilteringRemoteDetector.getModule()) .getInstance(PluginManager.class); ImmutableList> vulnDetectors = pluginManager.getVulnDetectors(fakeReconnaissanceReport); assertThat(vulnDetectors).hasSize(1); ImmutableList matchedResult = ((FakeFilteringRemoteDetector) vulnDetectors.get(0).tsunamiPlugin()).getMatchedPlugins(); assertThat(matchedResult).hasSize(4); assertThat(matchedResult.get(1).getPlugin()) .isEqualTo(FakeFilteringRemoteDetector.getJenkinsServiceDefinition()); assertThat(matchedResult.get(1).getServicesList()) .containsExactly(jenkinsService, noNameService); // The other non-OS detector should match, too: assertThat(matchedResult.get(0).getPlugin()) .isEqualTo(FakeFilteringRemoteDetector.getHttpServiceDefinition()); // wordpress: matching service_name (http) // jenkins: mismatching service_name (http*s*) // nonameservice: no service_name present in the NetworkService, hasMatchingServiceName accepts // that assertThat(matchedResult.get(0).getServicesList()) .containsExactly(wordPressService, noNameService); // The OS related ones should be empty: assertThat(matchedResult.get(2).getServicesCount()).isEqualTo(0); assertThat(matchedResult.get(3).getServicesCount()).isEqualTo(0); } @Test public void getVulnDetectors_whenRemoteDetectorOsFilterHasMatchingService_returnsMatchedService() { NetworkService wordPressService = NetworkService.newBuilder() .setNetworkEndpoint(NetworkEndpointUtils.forIpAndPort("1.1.1.1", 80)) .setTransportProtocol(TransportProtocol.TCP) .setServiceName("http") .setSoftware(Software.newBuilder().setName("WordPress")) .build(); NetworkService jenkinsService = NetworkService.newBuilder() .setNetworkEndpoint(NetworkEndpointUtils.forIpAndPort("1.1.1.1", 443)) .setTransportProtocol(TransportProtocol.TCP) .setServiceName("https") .setSoftware(Software.newBuilder().setName("Jenkins")) .build(); NetworkService noNameService = NetworkService.newBuilder() .setNetworkEndpoint(NetworkEndpointUtils.forIpAndPort("1.1.1.1", 12345)) .setTransportProtocol(TransportProtocol.TCP) .build(); ReconnaissanceReport fakeReconnaissanceReport = ReconnaissanceReport.newBuilder() .setTargetInfo( TargetInfo.newBuilder() .addOperatingSystemClasses( OperatingSystemClass.newBuilder() .setType("general purpose") .setVendor("Vendor") .setOsFamily("FakeOS") .setAccuracy(96))) .addNetworkServices(wordPressService) .addNetworkServices(jenkinsService) .addNetworkServices(noNameService) .build(); PluginManager pluginManager = Guice.createInjector( new HttpClientModule.Builder().build(), new PayloadGeneratorModule(new SecureRandom()), new FakePortScannerBootstrapModule(), new FakeServiceFingerprinterBootstrapModule(), FakeFilteringRemoteDetector.getModule()) .getInstance(PluginManager.class); ImmutableList> vulnDetectors = pluginManager.getVulnDetectors(fakeReconnaissanceReport); assertThat(vulnDetectors).hasSize(1); ImmutableList matchedResult = ((FakeFilteringRemoteDetector) vulnDetectors.get(0).tsunamiPlugin()).getMatchedPlugins(); assertThat(matchedResult).hasSize(4); assertThat(matchedResult.get(1).getPlugin()) .isEqualTo(FakeFilteringRemoteDetector.getJenkinsServiceDefinition()); assertThat(matchedResult.get(1).getServicesList()) .containsExactly(jenkinsService, noNameService); // The other non-OS detector should match, too: assertThat(matchedResult.get(0).getPlugin()) .isEqualTo(FakeFilteringRemoteDetector.getHttpServiceDefinition()); // wordpress: matching service_name (http) // jenkins: mismatching service_name (http*s*) // nonameservice: no service_name present in the NetworkService, hasMatchingServiceName accepts // that assertThat(matchedResult.get(0).getServicesList()) .containsExactly(wordPressService, noNameService); // The one that matches the OS only, should match everything: assertThat(matchedResult.get(2).getPlugin()) .isEqualTo(FakeFilteringRemoteDetector.getOperatingSystemServiceDefinition()); assertThat(matchedResult.get(2).getServicesList()) .containsExactly(wordPressService, jenkinsService, noNameService); // The one that matches the OS and "http", should return these: assertThat(matchedResult.get(3).getPlugin()) .isEqualTo(FakeFilteringRemoteDetector.getOperatingSystemAndHttpServiceDefinition()); assertThat(matchedResult.get(3).getServicesList()) .containsExactly(wordPressService, noNameService); } @Test public void getVulnDetectors_whenRemoteDetectorWithSoftwareFilterHasNoMatchingService_returnsNoServices() { NetworkService wordPressService = NetworkService.newBuilder() .setNetworkEndpoint(NetworkEndpointUtils.forIpAndPort("1.1.1.1", 443)) .setTransportProtocol(TransportProtocol.TCP) .setServiceName("https") .setSoftware(Software.newBuilder().setName("WordPress")) .build(); ReconnaissanceReport fakeReconnaissanceReport = ReconnaissanceReport.newBuilder() .setTargetInfo(TargetInfo.getDefaultInstance()) .addNetworkServices(wordPressService) .build(); PluginManager pluginManager = Guice.createInjector( new HttpClientModule.Builder().build(), new PayloadGeneratorModule(new SecureRandom()), new FakePortScannerBootstrapModule(), new FakeServiceFingerprinterBootstrapModule(), FakeFilteringRemoteDetector.getModule()) .getInstance(PluginManager.class); ImmutableList> vulnDetectors = pluginManager.getVulnDetectors(fakeReconnaissanceReport); assertThat(vulnDetectors).hasSize(1); ImmutableList matchedResult = ((FakeFilteringRemoteDetector) vulnDetectors.get(0).tsunamiPlugin()).getMatchedPlugins(); assertThat(matchedResult).hasSize(4); for (var mr : matchedResult) { assertThat(mr.getServicesCount()).isEqualTo(0); } } @Test public void getVulnDetectors_whenDetectorsIncludeIsOverridden_returnsMatchingVulnDetector() { NetworkService fakeNetworkService1 = NetworkService.newBuilder() .setNetworkEndpoint(NetworkEndpointUtils.forIpAndPort("1.1.1.1", 80)) .setTransportProtocol(TransportProtocol.TCP) .setServiceName("http") .build(); NetworkService fakeNetworkService2 = NetworkService.newBuilder() .setNetworkEndpoint(NetworkEndpointUtils.forIpAndPort("1.1.1.1", 443)) .setTransportProtocol(TransportProtocol.TCP) .setServiceName("https") .build(); ReconnaissanceReport fakeReconnaissanceReport = ReconnaissanceReport.newBuilder() .setTargetInfo(TargetInfo.getDefaultInstance()) .addNetworkServices(fakeNetworkService1) .addNetworkServices(fakeNetworkService2) .build(); try (ScanResult scanResult = new ClassGraph().enableAllInfo().scan()) { PluginManager pluginManager = Guice.createInjector( new CliOptionsModule( scanResult, "TsunamiCliTest", new String[] {"--detectors-include=Blabla1, FakeVulnDetector, Blabla2"}), new HttpClientModule.Builder().build(), new PayloadGeneratorModule(new SecureRandom()), new FakePortScannerBootstrapModule(), new FakeServiceFingerprinterBootstrapModule(), new FakeVulnDetectorBootstrapModule(), new FakeVulnDetectorBootstrapModule2()) .getInstance(PluginManager.class); ImmutableList> vulnDetectors = pluginManager.getVulnDetectors(fakeReconnaissanceReport); assertThat( vulnDetectors.stream() .map(pluginMatchingResult -> pluginMatchingResult.tsunamiPlugin().getClass())) .containsExactly(FakeVulnDetector.class); assertThat(vulnDetectors.stream().map(PluginMatchingResult::matchedServices)) .containsExactly(fakeReconnaissanceReport.getNetworkServicesList()); } } @Test public void getVulnDetectors_whenDetectorsExcludeIsOverridden_returnsMatchingVulnDetector() { NetworkService fakeNetworkService1 = NetworkService.newBuilder() .setNetworkEndpoint(NetworkEndpointUtils.forIpAndPort("1.1.1.1", 80)) .setTransportProtocol(TransportProtocol.TCP) .setServiceName("http") .build(); NetworkService fakeNetworkService2 = NetworkService.newBuilder() .setNetworkEndpoint(NetworkEndpointUtils.forIpAndPort("1.1.1.1", 443)) .setTransportProtocol(TransportProtocol.TCP) .setServiceName("https") .build(); ReconnaissanceReport fakeReconnaissanceReport = ReconnaissanceReport.newBuilder() .setTargetInfo(TargetInfo.getDefaultInstance()) .addNetworkServices(fakeNetworkService1) .addNetworkServices(fakeNetworkService2) .build(); try (ScanResult scanResult = new ClassGraph().enableAllInfo().scan()) { PluginManager pluginManager = Guice.createInjector( new CliOptionsModule( scanResult, "TsunamiCliTest", new String[] {"--detectors-exclude=FakeVulnDetector"}), new HttpClientModule.Builder().build(), new PayloadGeneratorModule(new SecureRandom()), new FakePortScannerBootstrapModule(), new FakeServiceFingerprinterBootstrapModule(), new FakeVulnDetectorBootstrapModule(), new FakeVulnDetectorBootstrapModule2()) .getInstance(PluginManager.class); ImmutableList> vulnDetectors = pluginManager.getVulnDetectors(fakeReconnaissanceReport); assertThat( vulnDetectors.stream() .map(pluginMatchingResult -> pluginMatchingResult.tsunamiPlugin().getClass())) .containsExactly(FakeVulnDetector2.class); assertThat(vulnDetectors.stream().map(PluginMatchingResult::matchedServices)) .containsExactly(fakeReconnaissanceReport.getNetworkServicesList()); } } @PluginInfo( type = PluginType.SERVICE_FINGERPRINT, name = "NoAnnotationFingerprinter", version = "v0.1", description = "A fake ServiceFingerprinter.", author = "fake", bootstrapModule = NoAnnotationFingerprinter.NoAnnotationFingerprinterBootstrapModule.class) private static final class NoAnnotationFingerprinter implements ServiceFingerprinter { @Override public FingerprintingReport fingerprint(TargetInfo targetInfo, NetworkService networkService) { return null; } static NoAnnotationFingerprinterBootstrapModule getModule() { return new NoAnnotationFingerprinterBootstrapModule(); } private static final class NoAnnotationFingerprinterBootstrapModule extends PluginBootstrapModule { @Override protected void configurePlugin() { registerPlugin(NoAnnotationFingerprinter.class); } } } @PluginInfo( type = PluginType.SERVICE_FINGERPRINT, name = "FakeWebFingerprinter", version = "v0.1", description = "A fake ServiceFingerprinter for web services.", author = "fake", bootstrapModule = FakeWebFingerprinter.FakeWebFingerprinterBootstrapModule.class) @ForWebService private static final class FakeWebFingerprinter implements ServiceFingerprinter { @Override public FingerprintingReport fingerprint(TargetInfo targetInfo, NetworkService networkService) { return null; } static FakeWebFingerprinterBootstrapModule getModule() { return new FakeWebFingerprinterBootstrapModule(); } private static final class FakeWebFingerprinterBootstrapModule extends PluginBootstrapModule { @Override protected void configurePlugin() { registerPlugin(FakeWebFingerprinter.class); } } } @PluginInfo( type = PluginType.VULN_DETECTION, name = "FakeServiceNameFilteringDetector", version = "v0.1", description = "A fake VulnDetector.", author = "fake", bootstrapModule = FakeServiceNameFilteringDetector.FakeServiceNameFilteringDetectorBootstrapModule.class) @ForServiceName("http") private static final class FakeServiceNameFilteringDetector implements VulnDetector { @Override public DetectionReportList detect( TargetInfo targetInfo, ImmutableList matchedServices) { return null; } @Override public ImmutableList getAdvisories() { return ImmutableList.of(); } static FakeServiceNameFilteringDetectorBootstrapModule getModule() { return new FakeServiceNameFilteringDetectorBootstrapModule(); } private static final class FakeServiceNameFilteringDetectorBootstrapModule extends PluginBootstrapModule { @Override protected void configurePlugin() { registerPlugin(FakeServiceNameFilteringDetector.class); } } } @PluginInfo( type = PluginType.VULN_DETECTION, name = "FakeSoftwareFilteringDetector", version = "v0.1", description = "A fake VulnDetector.", author = "fake", bootstrapModule = FakeSoftwareFilteringDetector.FakeSofwareFilteringDetectorBootstrapModule.class) @ForSoftware(name = "Jenkins") private static final class FakeSoftwareFilteringDetector implements VulnDetector { @Override public DetectionReportList detect( TargetInfo targetInfo, ImmutableList matchedServices) { return null; } @Override public ImmutableList getAdvisories() { return ImmutableList.of(); } static FakeSofwareFilteringDetectorBootstrapModule getModule() { return new FakeSofwareFilteringDetectorBootstrapModule(); } private static final class FakeSofwareFilteringDetectorBootstrapModule extends PluginBootstrapModule { @Override protected void configurePlugin() { registerPlugin(FakeSoftwareFilteringDetector.class); } } } @PluginInfo( type = PluginType.VULN_DETECTION, name = "FakeOsFilteringDetector", version = "v0.1", description = "A fake VulnDetector.", author = "fake", bootstrapModule = FakeOsFilteringDetector.FakeOsFilteringDetectorBootstrapModule.class) @ForOperatingSystemClass(osfamily = "FakeOS") private static final class FakeOsFilteringDetector implements VulnDetector { @Override public DetectionReportList detect( TargetInfo targetInfo, ImmutableList matchedServices) { return null; } @Override public ImmutableList getAdvisories() { return ImmutableList.of(); } static FakeOsFilteringDetectorBootstrapModule getModule() { return new FakeOsFilteringDetectorBootstrapModule(); } private static final class FakeOsFilteringDetectorBootstrapModule extends PluginBootstrapModule { @Override protected void configurePlugin() { registerPlugin(FakeOsFilteringDetector.class); } } } @PluginInfo( type = PluginType.VULN_DETECTION, name = "FakeOsServiceFilteringDetector", version = "v0.1", description = "A fake VulnDetector.", author = "fake", bootstrapModule = FakeOsServiceFilteringDetector.FakeOsServiceFilteringDetectorBootstrapModule.class) @ForOperatingSystemClass(osfamily = "FakeOS") @ForSoftware(name = "Jenkins") private static final class FakeOsServiceFilteringDetector implements VulnDetector { @Override public DetectionReportList detect( TargetInfo targetInfo, ImmutableList matchedServices) { return null; } @Override public ImmutableList getAdvisories() { return ImmutableList.of(); } static FakeOsServiceFilteringDetectorBootstrapModule getModule() { return new FakeOsServiceFilteringDetectorBootstrapModule(); } private static final class FakeOsServiceFilteringDetectorBootstrapModule extends PluginBootstrapModule { @Override protected void configurePlugin() { registerPlugin(FakeOsServiceFilteringDetector.class); } } } @PluginInfo( type = PluginType.REMOTE_VULN_DETECTION, name = "FakeFilteringRemoteDetector", version = "v0.1", description = "A fake RemoteVulnDetector.", author = "fake", bootstrapModule = FakeFilteringRemoteDetector.FakeFilteringRemoteDetectorBootstrapModule.class) private static final class FakeFilteringRemoteDetector implements RemoteVulnDetector { private final List matchedPlugins; FakeFilteringRemoteDetector() { matchedPlugins = Lists.newArrayList(); } public ImmutableList getMatchedPlugins() { return ImmutableList.copyOf(matchedPlugins); } @Override public DetectionReportList detect( TargetInfo targetInfo, ImmutableList matchedServices) { return null; } @Override public ImmutableList getAdvisories() { return ImmutableList.of(); } @Override public ImmutableList getAllPlugins() { return ImmutableList.of( getHttpServiceDefinition(), getJenkinsServiceDefinition(), getOperatingSystemServiceDefinition(), getOperatingSystemAndHttpServiceDefinition()); } @Override public void addMatchedPluginToDetect(MatchedPlugin plugin) { matchedPlugins.add(plugin); } static com.google.tsunami.proto.PluginDefinition getHttpServiceDefinition() { return com.google.tsunami.proto.PluginDefinition.newBuilder() .setInfo( com.google.tsunami.proto.PluginInfo.newBuilder() .setType(com.google.tsunami.proto.PluginInfo.PluginType.VULN_DETECTION) .setName("FakeHttpServiceVuln") .setVersion("v0.1") .setDescription("A fake VulnDetector.") .setAuthor("fake")) .setTargetServiceName(TargetServiceName.newBuilder().addValue("http")) .build(); } static com.google.tsunami.proto.PluginDefinition getJenkinsServiceDefinition() { return com.google.tsunami.proto.PluginDefinition.newBuilder() .setInfo( com.google.tsunami.proto.PluginInfo.newBuilder() .setType(com.google.tsunami.proto.PluginInfo.PluginType.VULN_DETECTION) .setName("FakeJenkinsVuln") .setVersion("v0.1") .setDescription("A fake VulnDetector") .setAuthor("fake")) .setTargetSoftware(TargetSoftware.newBuilder().setName("Jenkins")) .build(); } static com.google.tsunami.proto.PluginDefinition getOperatingSystemServiceDefinition() { return com.google.tsunami.proto.PluginDefinition.newBuilder() .setInfo( com.google.tsunami.proto.PluginInfo.newBuilder() .setType(com.google.tsunami.proto.PluginInfo.PluginType.VULN_DETECTION) .setName("FakeOsVuln") .setVersion("v0.1") .setDescription("A fake VulnDetector that targets services running on FakeOS") .setAuthor("fake")) .setTargetOperatingSystemClass( TargetOperatingSystemClass.newBuilder() .addOsFamily("ThisWontMatch") .addOsFamily("FakeOS")) .build(); } static com.google.tsunami.proto.PluginDefinition getOperatingSystemAndHttpServiceDefinition() { return com.google.tsunami.proto.PluginDefinition.newBuilder() .setInfo( com.google.tsunami.proto.PluginInfo.newBuilder() .setType(com.google.tsunami.proto.PluginInfo.PluginType.VULN_DETECTION) .setName("FakeOsHttpVuln") .setVersion("v0.1") .setDescription( "A fake VulnDetector that targets http services running on FakeOS") .setAuthor("fake")) .setTargetServiceName(TargetServiceName.newBuilder().addValue("http")) .setTargetOperatingSystemClass( TargetOperatingSystemClass.newBuilder() .addVendor("ThisWontMatch") .addOsFamily("FakeOS") .setMinAccuracy(90)) .build(); } static FakeFilteringRemoteDetectorBootstrapModule getModule() { return new FakeFilteringRemoteDetectorBootstrapModule(); } private static final class FakeFilteringRemoteDetectorBootstrapModule extends PluginBootstrapModule { @Override protected void configurePlugin() { registerPlugin(FakeFilteringRemoteDetector.class); } } } private static final class FakeRemoteVulnDetectorLoadingModule extends AbstractModule { private final int numRemotePlugins; public FakeRemoteVulnDetectorLoadingModule() { this(0); } public FakeRemoteVulnDetectorLoadingModule(int numRemotePlugins) { this.numRemotePlugins = numRemotePlugins; } @Override protected void configure() { MapBinder tsunamiPluginBinder = MapBinder.newMapBinder(binder(), PluginDefinition.class, TsunamiPlugin.class); for (int i = 0; i < numRemotePlugins; i++) { tsunamiPluginBinder .addBinding(RemoteVulnDetectorLoadingModule.getRemoteVulnDetectorPluginDefinition(i)) .toInstance(new FakeRemoteVulnDetector(i)); } } } } ================================================ FILE: plugin/src/test/java/com/google/tsunami/plugin/PluginServiceClientTest.java ================================================ /* * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.plugin; import static com.google.common.truth.Truth.assertThat; import static java.util.concurrent.TimeUnit.SECONDS; import com.google.common.collect.Lists; import com.google.common.util.concurrent.ListenableFuture; import com.google.tsunami.common.data.NetworkEndpointUtils; import com.google.tsunami.proto.DetectionReport; import com.google.tsunami.proto.DetectionReportList; import com.google.tsunami.proto.ListPluginsRequest; import com.google.tsunami.proto.ListPluginsResponse; import com.google.tsunami.proto.MatchedPlugin; import com.google.tsunami.proto.NetworkEndpoint; import com.google.tsunami.proto.NetworkService; import com.google.tsunami.proto.PluginDefinition; import com.google.tsunami.proto.PluginInfo; import com.google.tsunami.proto.PluginServiceGrpc.PluginServiceImplBase; import com.google.tsunami.proto.RunRequest; import com.google.tsunami.proto.RunResponse; import com.google.tsunami.proto.TargetInfo; import com.google.tsunami.proto.TransportProtocol; import io.grpc.Deadline; import io.grpc.health.v1.HealthCheckRequest; import io.grpc.health.v1.HealthCheckResponse; import io.grpc.health.v1.HealthCheckResponse.ServingStatus; import io.grpc.health.v1.HealthGrpc.HealthImplBase; import io.grpc.inprocess.InProcessChannelBuilder; import io.grpc.inprocess.InProcessServerBuilder; import io.grpc.stub.StreamObserver; import io.grpc.testing.GrpcCleanupRule; import io.grpc.util.MutableHandlerRegistry; import java.util.ArrayList; import java.util.List; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** Tests for {@link PluginServiceClient}. */ @RunWith(JUnit4.class) public final class PluginServiceClientTest { @Rule public final GrpcCleanupRule grpcCleanup = new GrpcCleanupRule(); private static final String PLUGIN_NAME = "test plugin"; private static final String PLUGIN_VERSION = "0.0.1"; private static final String PLUGIN_DESCRIPTION = "test description"; private static final String PLUGIN_AUTHOR = "tester"; private static final Deadline DEADLINE_DEFAULT = Deadline.after(5, SECONDS); private PluginServiceClient pluginService; private final MutableHandlerRegistry serviceRegistry = new MutableHandlerRegistry(); @Before public void setUp() throws Exception { String serverName = InProcessServerBuilder.generateName(); grpcCleanup.register( InProcessServerBuilder.forName(serverName) .fallbackHandlerRegistry(serviceRegistry) .directExecutor() .build() .start()); pluginService = new PluginServiceClient( InProcessChannelBuilder.forName(serverName).directExecutor().build()); } @Test public void pluginService_isNotNull() { assertThat(pluginService).isNotNull(); } @Test public void run_invalidRequest_returnNoDetectionReports() throws Exception { RunRequest runRequest = RunRequest.getDefaultInstance(); PluginServiceImplBase runImpl = new PluginServiceImplBase() { @Override public void run(RunRequest request, StreamObserver responseObserver) { responseObserver.onNext(RunResponse.getDefaultInstance()); responseObserver.onCompleted(); } }; serviceRegistry.addService(runImpl); ListenableFuture run = pluginService.runWithDeadline(runRequest, DEADLINE_DEFAULT); RunResponse runResponse = run.get(); assertThat(run.isDone()).isTrue(); assertThat(runResponse.hasReports()).isFalse(); } @Test public void run_singlePluginValidRequest_returnSingleDetectionReport() throws Exception { RunRequest runRequest = createSinglePluginRunRequest(); PluginServiceImplBase runImpl = new PluginServiceImplBase() { @Override public void run(RunRequest request, StreamObserver responseObserver) { DetectionReportList reportList = DetectionReportList.newBuilder() .addDetectionReports( DetectionReport.newBuilder() .setTargetInfo(request.getTarget()) .setNetworkService(request.getPlugins(0).getServices(0))) .build(); responseObserver.onNext(RunResponse.newBuilder().setReports(reportList).build()); responseObserver.onCompleted(); } }; serviceRegistry.addService(runImpl); ListenableFuture run = pluginService.runWithDeadline(runRequest, DEADLINE_DEFAULT); RunResponse runResponse = run.get(); assertThat(run.isDone()).isTrue(); assertRunResponseContainsAllRunRequestParameters(runResponse, runRequest); } @Test public void run_multiplePluginValidRequest_returnMultipleDetectionReports() throws Exception { int numPluginsToTest = 5; List endpoints = new ArrayList<>(numPluginsToTest); endpoints.add(NetworkEndpointUtils.forIpAndPort("1.1.1.1", 80)); endpoints.add(NetworkEndpointUtils.forIpAndPort("1.1.1.1", 443)); endpoints.add(NetworkEndpointUtils.forIpAndPort("1.1.1.1", 123)); endpoints.add(NetworkEndpointUtils.forIpAndPort("1.1.1.1", 456)); endpoints.add(NetworkEndpointUtils.forIpAndPort("1.1.1.1", 789)); PluginInfo.Builder pluginInfoBuilder = PluginInfo.newBuilder() .setType(PluginInfo.PluginType.VULN_DETECTION) .setVersion(PLUGIN_VERSION) .setDescription(PLUGIN_DESCRIPTION) .setAuthor(PLUGIN_AUTHOR); TargetInfo target = TargetInfo.newBuilder().addAllNetworkEndpoints(endpoints).build(); RunRequest.Builder runRequestBuilder = RunRequest.newBuilder().setTarget(target); for (int i = 0; i < numPluginsToTest; i++) { PluginInfo pluginInfo = pluginInfoBuilder.setName(String.format(PLUGIN_NAME + " %d", i)).build(); NetworkService httpService = NetworkService.newBuilder() .setNetworkEndpoint(endpoints.get(i)) .setTransportProtocol(TransportProtocol.TCP) .setServiceName("http") .build(); runRequestBuilder.addPlugins( MatchedPlugin.newBuilder() .addServices(httpService) .setPlugin(PluginDefinition.newBuilder().setInfo(pluginInfo).build())); } RunRequest runRequest = runRequestBuilder.build(); PluginServiceImplBase runImpl = new PluginServiceImplBase() { @Override public void run(RunRequest request, StreamObserver responseObserver) { DetectionReportList.Builder reportListBuilder = DetectionReportList.newBuilder(); for (MatchedPlugin plugin : request.getPluginsList()) { reportListBuilder.addDetectionReports( DetectionReport.newBuilder() .setTargetInfo(request.getTarget()) .setNetworkService(plugin.getServices(0))); } responseObserver.onNext(RunResponse.newBuilder().setReports(reportListBuilder).build()); responseObserver.onCompleted(); } }; serviceRegistry.addService(runImpl); ListenableFuture run = pluginService.runWithDeadline(runRequest, DEADLINE_DEFAULT); RunResponse runResponse = run.get(); assertThat(run.isDone()).isTrue(); assertThat(runResponse.getReports().getDetectionReportsCount()).isEqualTo(numPluginsToTest); assertRunResponseContainsAllRunRequestParameters(runResponse, runRequest); } @Test public void listPlugins_returnsMultiplePlugins() throws Exception { ListPluginsRequest request = ListPluginsRequest.getDefaultInstance(); List plugins = Lists.newArrayList(); for (int i = 0; i < 5; i++) { plugins.add(createSinglePluginDefinitionWithName(String.format(PLUGIN_NAME + "%d", i))); } PluginServiceImplBase listPluginsImpl = new PluginServiceImplBase() { @Override public void listPlugins( ListPluginsRequest request, StreamObserver responseObserver) { responseObserver.onNext( ListPluginsResponse.newBuilder().addAllPlugins(plugins).build()); responseObserver.onCompleted(); } }; serviceRegistry.addService(listPluginsImpl); ListenableFuture listPlugins = pluginService.listPluginsWithDeadline(request, DEADLINE_DEFAULT); assertThat(listPlugins.isDone()).isTrue(); assertThat(listPlugins.get().getPluginsList()).containsExactlyElementsIn(plugins); } @Test public void checkHealth_returnServingHealthResponse() throws Exception { HealthCheckRequest request = HealthCheckRequest.getDefaultInstance(); HealthImplBase healthImpl = new HealthImplBase() { @Override public void check( HealthCheckRequest request, StreamObserver responseObserver) { responseObserver.onNext( HealthCheckResponse.newBuilder().setStatus(ServingStatus.SERVING).build()); responseObserver.onCompleted(); } }; serviceRegistry.addService(healthImpl); ListenableFuture health = pluginService.checkHealthWithDeadline(request, DEADLINE_DEFAULT); assertThat(health.isDone()).isTrue(); assertThat(health.get().getStatus()).isEqualTo(ServingStatus.SERVING); } @Test public void checkHealth_returnNotServingHealthResponse() throws Exception { HealthCheckRequest request = HealthCheckRequest.getDefaultInstance(); HealthImplBase healthImpl = new HealthImplBase() { @Override public void check( HealthCheckRequest request, StreamObserver responseObserver) { responseObserver.onNext( HealthCheckResponse.newBuilder().setStatus(ServingStatus.NOT_SERVING).build()); responseObserver.onCompleted(); } }; serviceRegistry.addService(healthImpl); ListenableFuture health = pluginService.checkHealthWithDeadline(request, DEADLINE_DEFAULT); assertThat(health.isDone()).isTrue(); assertThat(health.get().getStatus()).isEqualTo(ServingStatus.NOT_SERVING); } private void assertRunResponseContainsAllRunRequestParameters( RunResponse response, RunRequest request) throws Exception { for (MatchedPlugin plugin : request.getPluginsList()) { DetectionReport expectedReport = DetectionReport.newBuilder() .setTargetInfo(request.getTarget()) .setNetworkService(plugin.getServices(0)) .build(); assertThat(response.getReports().getDetectionReportsList()).contains(expectedReport); } } private PluginDefinition createSinglePluginDefinitionWithName(String name) { PluginInfo pluginInfo = PluginInfo.newBuilder() .setType(PluginInfo.PluginType.VULN_DETECTION) .setName(name) .setVersion(PLUGIN_VERSION) .setDescription(PLUGIN_DESCRIPTION) .setAuthor(PLUGIN_AUTHOR) .build(); return PluginDefinition.newBuilder().setInfo(pluginInfo).build(); } private RunRequest createSinglePluginRunRequest() { PluginDefinition singlePlugin = createSinglePluginDefinitionWithName(PLUGIN_NAME); NetworkService httpService = NetworkService.newBuilder() .setNetworkEndpoint(NetworkEndpointUtils.forIpAndPort("1.1.1.1", 80)) .setTransportProtocol(TransportProtocol.TCP) .setServiceName("http") .build(); TargetInfo target = TargetInfo.newBuilder().addNetworkEndpoints(httpService.getNetworkEndpoint()).build(); return RunRequest.newBuilder() .setTarget(target) .addPlugins(MatchedPlugin.newBuilder().addServices(httpService).setPlugin(singlePlugin)) .build(); } } ================================================ FILE: plugin/src/test/java/com/google/tsunami/plugin/RemoteVulnDetectorImplTest.java ================================================ /* * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.plugin; import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat; import static org.junit.Assert.assertThrows; import com.google.api.client.util.ExponentialBackOff; import com.google.common.collect.ImmutableList; import com.google.inject.AbstractModule; import com.google.inject.Guice; import com.google.tsunami.common.data.NetworkEndpointUtils; import com.google.tsunami.proto.DetectionReport; import com.google.tsunami.proto.DetectionReportList; import com.google.tsunami.proto.ListPluginsRequest; import com.google.tsunami.proto.ListPluginsResponse; import com.google.tsunami.proto.MatchedPlugin; import com.google.tsunami.proto.NetworkService; import com.google.tsunami.proto.PluginDefinition; import com.google.tsunami.proto.PluginInfo; import com.google.tsunami.proto.PluginServiceGrpc.PluginServiceImplBase; import com.google.tsunami.proto.RunCompactRequest; import com.google.tsunami.proto.RunRequest; import com.google.tsunami.proto.RunResponse; import com.google.tsunami.proto.TargetInfo; import com.google.tsunami.proto.TransportProtocol; import io.grpc.health.v1.HealthCheckRequest; import io.grpc.health.v1.HealthCheckResponse; import io.grpc.health.v1.HealthCheckResponse.ServingStatus; import io.grpc.health.v1.HealthGrpc.HealthImplBase; import io.grpc.inprocess.InProcessChannelBuilder; import io.grpc.inprocess.InProcessServerBuilder; import io.grpc.stub.StreamObserver; import io.grpc.testing.GrpcCleanupRule; import io.grpc.util.MutableHandlerRegistry; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @RunWith(JUnit4.class) public final class RemoteVulnDetectorImplTest { private static final String PLUGIN_VERSION = "0.0.1"; private static final String PLUGIN_DESCRIPTION = "test description"; private static final String PLUGIN_AUTHOR = "tester"; private static final int INITIAL_WAIT_TIME_MS = 20; private static final int MAX_WAIT_TIME_MS = 30000; private static final int WAIT_TIME_MULTIPLIER = 3; private static final int MAX_ATTEMPTS = 3; private static final ExponentialBackOff BACKOFF = new ExponentialBackOff.Builder() .setInitialIntervalMillis(INITIAL_WAIT_TIME_MS) .setRandomizationFactor(0.1) .setMultiplier(WAIT_TIME_MULTIPLIER) .setMaxElapsedTimeMillis(MAX_WAIT_TIME_MS) .build(); private final MutableHandlerRegistry serviceRegistry = new MutableHandlerRegistry(); @Rule public final GrpcCleanupRule grpcCleanup = new GrpcCleanupRule(); @Before public void setUp() throws Exception { serviceRegistry.addService( new PluginServiceImplBase() { @Override public void run(RunRequest request, StreamObserver responseObserver) { DetectionReportList.Builder reportListBuilder = DetectionReportList.newBuilder(); for (MatchedPlugin plugin : request.getPluginsList()) { reportListBuilder.addDetectionReports( DetectionReport.newBuilder() .setTargetInfo(request.getTarget()) .setNetworkService(plugin.getServices(0))); } responseObserver.onNext(RunResponse.newBuilder().setReports(reportListBuilder).build()); responseObserver.onCompleted(); } }); } @Test public void detect_withServingServer_returnsSuccessfulDetectionReportList() throws Exception { registerHealthCheckWithStatus(ServingStatus.SERVING); registerSuccessfulRunService(); RemoteVulnDetector pluginToTest = getNewRemoteVulnDetectorInstance(); var endpointToTest = NetworkEndpointUtils.forIpAndPort("1.1.1.1", 80); var serviceToTest = NetworkService.newBuilder() .setNetworkEndpoint(endpointToTest) .setTransportProtocol(TransportProtocol.TCP) .setServiceName("http") .build(); TargetInfo testTarget = TargetInfo.newBuilder().addNetworkEndpoints(endpointToTest).build(); pluginToTest.addMatchedPluginToDetect( MatchedPlugin.newBuilder() .addServices(serviceToTest) .setPlugin(createSinglePluginDefinitionWithName("test")) .build()); assertThat(pluginToTest.detect(testTarget, ImmutableList.of()).getDetectionReportsList()) .comparingExpectedFieldsOnly() .containsExactly( DetectionReport.newBuilder() .setTargetInfo(testTarget) .setNetworkService(serviceToTest) .build()); } @Test public void detect_withNonServingServer_returnsEmptyDetectionReportList() throws Exception { registerHealthCheckWithStatus(ServingStatus.NOT_SERVING); RemoteVulnDetector pluginToTest = getNewRemoteVulnDetectorInstance(); var endpointToTest = NetworkEndpointUtils.forIpAndPort("1.1.1.1", 80); var serviceToTest = NetworkService.newBuilder() .setNetworkEndpoint(endpointToTest) .setTransportProtocol(TransportProtocol.TCP) .setServiceName("http") .build(); TargetInfo testTarget = TargetInfo.newBuilder().addNetworkEndpoints(endpointToTest).build(); pluginToTest.addMatchedPluginToDetect( MatchedPlugin.newBuilder() .addServices(serviceToTest) .setPlugin(createSinglePluginDefinitionWithName("test")) .build()); assertThat(pluginToTest.detect(testTarget, ImmutableList.of()).getDetectionReportsList()) .isEmpty(); } @Test(timeout = 30000L) public void detect_withRpcError_throwsLanguageServerException() throws Exception { registerHealthCheckWithError(); assertThrows( LanguageServerException.class, () -> getNewRemoteVulnDetectorInstance() .detect(TargetInfo.getDefaultInstance(), ImmutableList.of())); } @Test public void getAllPlugins_withServingServer_returnsSuccessfulList() throws Exception { registerHealthCheckWithStatus(ServingStatus.SERVING); var plugin = createSinglePluginDefinitionWithName("test"); RemoteVulnDetector pluginToTest = getNewRemoteVulnDetectorInstance(); serviceRegistry.addService( new PluginServiceImplBase() { @Override public void listPlugins( ListPluginsRequest request, StreamObserver responseObserver) { responseObserver.onNext(ListPluginsResponse.newBuilder().addPlugins(plugin).build()); responseObserver.onCompleted(); } }); assertThat(pluginToTest.getAllPlugins()).containsExactly(plugin); } @Test public void getAllPlugins_withCompactRunRequest_callsRunCompact() throws Exception { registerHealthCheckWithStatus(ServingStatus.SERVING); var targetInfo = TargetInfo.newBuilder() .addNetworkEndpoints(NetworkEndpointUtils.forIpAndPort("1.1.1.1", 80)) .build(); var someNetworkService = NetworkService.getDefaultInstance(); var expectedDetectionReport = DetectionReport.newBuilder() .setTargetInfo(targetInfo) .setNetworkService(someNetworkService) .build(); var plugin = createSinglePluginDefinitionWithName("test"); RemoteVulnDetector pluginToTest = getNewRemoteVulnDetectorInstance(); serviceRegistry.addService( new PluginServiceImplBase() { @Override public void listPlugins( ListPluginsRequest request, StreamObserver responseObserver) { responseObserver.onNext( ListPluginsResponse.newBuilder() .setWantCompactRunRequest(true) .addPlugins(plugin) .build()); responseObserver.onCompleted(); } @Override public void run(RunRequest request, StreamObserver responseObserver) { responseObserver.onError(new Exception("run should not be called")); } @Override public void runCompact( RunCompactRequest request, StreamObserver responseObserver) { responseObserver.onNext( RunResponse.newBuilder() .setReports( DetectionReportList.newBuilder() .addDetectionReports(expectedDetectionReport)) .build()); responseObserver.onCompleted(); } }); assertThat(pluginToTest.getAllPlugins()).containsExactly(plugin); assertThat( pluginToTest .detect(targetInfo, ImmutableList.of(someNetworkService)) .getDetectionReportsList()) .containsExactly(expectedDetectionReport); } @Test public void getAllPlugins_withNonServingServer_returnsEmptyList() throws Exception { registerHealthCheckWithStatus(ServingStatus.NOT_SERVING); assertThat(getNewRemoteVulnDetectorInstance().getAllPlugins()).isEmpty(); } @Test(timeout = 30000L) public void getAllPlugins_withRpcError_throwsLanguageServerException() throws Exception { registerHealthCheckWithError(); assertThrows(LanguageServerException.class, getNewRemoteVulnDetectorInstance()::getAllPlugins); } @Test(timeout = 30000L) public void getAllPlugins_withUnregisteredHealthService_throwsLanguageServerException() throws Exception { assertThrows(LanguageServerException.class, getNewRemoteVulnDetectorInstance()::getAllPlugins); } private RemoteVulnDetector getNewRemoteVulnDetectorInstance() throws Exception { String serverName = InProcessServerBuilder.generateName(); grpcCleanup.register( InProcessServerBuilder.forName(serverName) .fallbackHandlerRegistry(serviceRegistry) .directExecutor() .build() .start()); return Guice.createInjector( new AbstractModule() { @Override protected void configure() { bind(RemoteVulnDetector.class) .toInstance( new RemoteVulnDetectorImpl( InProcessChannelBuilder.forName(serverName).directExecutor().build(), BACKOFF, MAX_ATTEMPTS, null)); } }) .getInstance(RemoteVulnDetector.class); } private void registerHealthCheckWithError() { serviceRegistry.addService( new HealthImplBase() { @Override public void check( HealthCheckRequest request, StreamObserver responseObserver) { responseObserver.onError(new RuntimeException("Test failure.")); responseObserver.onCompleted(); } }); } private void registerHealthCheckWithStatus(ServingStatus status) { serviceRegistry.addService( new HealthImplBase() { @Override public void check( HealthCheckRequest request, StreamObserver responseObserver) { responseObserver.onNext(HealthCheckResponse.newBuilder().setStatus(status).build()); responseObserver.onCompleted(); } }); } private void registerSuccessfulRunService() { serviceRegistry.addService( new PluginServiceImplBase() { @Override public void run(RunRequest request, StreamObserver responseObserver) { DetectionReportList.Builder reportListBuilder = DetectionReportList.newBuilder(); for (MatchedPlugin plugin : request.getPluginsList()) { reportListBuilder.addDetectionReports( DetectionReport.newBuilder() .setTargetInfo(request.getTarget()) .setNetworkService(plugin.getServices(0))); } responseObserver.onNext(RunResponse.newBuilder().setReports(reportListBuilder).build()); responseObserver.onCompleted(); } }); } private PluginDefinition createSinglePluginDefinitionWithName(String name) { return PluginDefinition.newBuilder() .setInfo( PluginInfo.newBuilder() .setType(PluginInfo.PluginType.VULN_DETECTION) .setName(name) .setVersion(PLUGIN_VERSION) .setDescription(PLUGIN_DESCRIPTION) .setAuthor(PLUGIN_AUTHOR)) .build(); } } ================================================ FILE: plugin/src/test/java/com/google/tsunami/plugin/RemoteVulnDetectorLoadingModuleTest.java ================================================ /* * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.plugin; import static com.google.common.truth.Truth.assertThat; import com.google.common.collect.ImmutableList; import com.google.inject.Guice; import com.google.inject.Key; import com.google.inject.util.Types; import com.google.tsunami.common.server.LanguageServerCommand; import io.grpc.inprocess.InProcessServerBuilder; import java.time.Duration; import java.util.Map; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @RunWith(JUnit4.class) public final class RemoteVulnDetectorLoadingModuleTest { @SuppressWarnings("unchecked") private static final Key> PLUGIN_BINDING_KEY = (Key>) Key.get(Types.mapOf(PluginDefinition.class, TsunamiPlugin.class)); private static String generateServerName() { return InProcessServerBuilder.generateName(); } @Test public void configure_whenNoChannelsRegistered_loadsNoRemotePlugins() { Map remotePlugins = Guice.createInjector(new RemoteVulnDetectorLoadingModule(ImmutableList.of())) .getInstance(PLUGIN_BINDING_KEY); assertThat(remotePlugins).isEmpty(); } @Test public void configure_always_loadsAllRemotePlugins() { var path0 = LanguageServerCommand.create( generateServerName(), "", "34567", "193", "/output/here", false, Duration.ofSeconds(10), "157.34.0.2", 8080, "157.34.0.2:8881", 0); var path1 = LanguageServerCommand.create( generateServerName(), "", "34566", "193", "/output/now", false, Duration.ofSeconds(10), "157.34.0.2", 8080, "157.34.0.2:8881", 0); var server0 = LanguageServerCommand.create( "", "127.0.0.1", "34567", "193", "/output/here", false, Duration.ofSeconds(10), "157.34.0.2", 8080, "157.34.0.2:8881", 0); Map remotePlugins = Guice.createInjector( new RemoteVulnDetectorLoadingModule(ImmutableList.of(path0, path1, server0))) .getInstance(PLUGIN_BINDING_KEY); assertThat(remotePlugins).hasSize(3); } } ================================================ FILE: plugin/src/test/java/com/google/tsunami/plugin/TcsClientTest.java ================================================ /* * Copyright 2021 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.plugin; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; import com.google.inject.Guice; import com.google.protobuf.util.JsonFormat; import com.google.tsunami.callbackserver.proto.PollingResult; import com.google.tsunami.common.net.http.HttpClient; import com.google.tsunami.common.net.http.HttpClientModule; import com.google.tsunami.common.net.http.HttpStatus; import java.io.IOException; import javax.inject.Inject; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** Tests for {@link TcsClient}. */ @RunWith(JUnit4.class) public final class TcsClientTest { private static final String SECRET = "a3d9ed89deadbeef"; private static final String CBID = "04041e8898e739ca33a250923e24f59ca41a8373f8cf6a45a1275f3b"; private static final String VALID_IPV4_ADDRESS = "127.0.0.1"; private static final String VALID_IPV6_ADDRESS = "2001:0db8:85a3:0000:0000:8a2e:0370:7334"; private static final int VALID_PORT = 8000; private static final String VALID_DOMAIN = "valid.co"; private static final String VALID_URL = "http://valid.co"; private static final String INVALID_ADDRESS = "http://invalid-address.com"; @Inject private HttpClient httpClient; private TcsClient client; @Before public void setup() { Guice.createInjector(new HttpClientModule.Builder().build()).injectMembers(this); } @Test public void getCallbackUri_validIpv4Address_returnsUriWithCbidInPath() { client = new TcsClient(VALID_IPV4_ADDRESS, VALID_PORT, VALID_URL, httpClient); String url = client.getCallbackUri(SECRET); String expectedUriString = String.format("http://%s:%d/%s", VALID_IPV4_ADDRESS, VALID_PORT, CBID); assertThat(url).isEqualTo(expectedUriString); } @Test public void getCallbackUri_validIpv6Address_returnsUriWithCbidInPath() { client = new TcsClient(VALID_IPV6_ADDRESS, VALID_PORT, VALID_URL, httpClient); String url = client.getCallbackUri(SECRET); String expectedUriString = String.format("http://[%s]:%d/%s", VALID_IPV6_ADDRESS, VALID_PORT, CBID); assertThat(url).isEqualTo(expectedUriString); } @Test public void getCallbackUri_validDomainAddress_returnsUriWithCbidInSubdomain() { client = new TcsClient(VALID_DOMAIN, VALID_PORT, VALID_URL, httpClient); String url = client.getCallbackUri(SECRET); String expectedUriString = String.format("%s.%s:%d", CBID, VALID_DOMAIN, VALID_PORT); assertThat(url).isEqualTo(expectedUriString); } @Test public void getCallbackUri_callbackPortIs80_returnsUriWithoutPortNum() { client = new TcsClient(VALID_IPV4_ADDRESS, 80, VALID_URL, httpClient); String url = client.getCallbackUri(SECRET); String expectedUriString = String.format("http://%s/%s", VALID_IPV4_ADDRESS, CBID); assertThat(url).isEqualTo(expectedUriString); } @Test public void getCallbackUri_invalidAddress_throwsError() { client = new TcsClient(INVALID_ADDRESS, VALID_PORT, VALID_URL, httpClient); assertThrows(IllegalArgumentException.class, () -> client.getCallbackUri(SECRET)); } @Test public void getCallbackUri_invalidCallbackPort_throwsError() { client = new TcsClient(VALID_DOMAIN, 100000, VALID_URL, httpClient); assertThrows(AssertionError.class, () -> client.getCallbackUri(SECRET)); } @Test public void getCallbackAddress_validIpv4Address_returnsAddress() { client = new TcsClient(VALID_IPV4_ADDRESS, VALID_PORT, VALID_URL, httpClient); assertThat(client.getCallbackAddress()).isEqualTo(VALID_IPV4_ADDRESS); } @Test public void getCallbackAddress_validIpv6Address_returnsAddress() { client = new TcsClient(VALID_IPV6_ADDRESS, VALID_PORT, VALID_URL, httpClient); assertThat(client.getCallbackAddress()).isEqualTo(VALID_IPV6_ADDRESS); } @Test public void getCallbackAddress_validDomainAddress_returnsAddress() { client = new TcsClient(VALID_DOMAIN, VALID_PORT, VALID_URL, httpClient); assertThat(client.getCallbackAddress()).isEqualTo(VALID_DOMAIN); } @Test public void getCallbackAddress_invalidAddress_throwsError() { client = new TcsClient(INVALID_ADDRESS, 100000, VALID_URL, httpClient); assertThrows(AssertionError.class, () -> client.getCallbackAddress()); } @Test public void getCallbackPort_validPort_returnsPort() { client = new TcsClient(VALID_DOMAIN, VALID_PORT, VALID_URL, httpClient); assertThat(client.getCallbackPort()).isEqualTo(VALID_PORT); } @Test public void getCallbackPort_invalidPort_throwsError() { client = new TcsClient(VALID_DOMAIN, 100000, VALID_URL, httpClient); assertThrows(AssertionError.class, () -> client.getCallbackPort()); } @Test public void isVulnerable_sendsValidPollingRequest() throws IOException, InterruptedException { MockWebServer mockWebServer = new MockWebServer(); mockWebServer.start(); client = new TcsClient(VALID_DOMAIN, VALID_PORT, mockWebServer.url("/").toString(), httpClient); client.hasOobLog(SECRET); assertThat(mockWebServer.takeRequest().getPath()) .isEqualTo(String.format("/?secret=%s", SECRET)); mockWebServer.shutdown(); } @Test public void isVulnerable_validLogRecordWithHttpLogged_returnsTrue() throws IOException { PollingResult log = PollingResult.newBuilder().setHasHttpInteraction(true).build(); String body = JsonFormat.printer().preservingProtoFieldNames().print(log); MockWebServer mockWebServer = new MockWebServer(); mockWebServer.enqueue(new MockResponse().setResponseCode(HttpStatus.OK.code()).setBody(body)); client = new TcsClient(VALID_DOMAIN, VALID_PORT, mockWebServer.url("/").toString(), httpClient); boolean detectionResult = client.hasOobLog(SECRET); assertThat(detectionResult).isTrue(); mockWebServer.shutdown(); } @Test public void isVulnerable_validLogRecordWithNothingLogged_returnsFalse() throws IOException { PollingResult log = PollingResult.getDefaultInstance(); String body = JsonFormat.printer().preservingProtoFieldNames().print(log); MockWebServer mockWebServer = new MockWebServer(); mockWebServer.enqueue(new MockResponse().setResponseCode(HttpStatus.OK.code()).setBody(body)); client = new TcsClient(VALID_DOMAIN, VALID_PORT, mockWebServer.url("/").toString(), httpClient); boolean detectionResult = client.hasOobLog(SECRET); assertThat(detectionResult).isFalse(); mockWebServer.shutdown(); } @Test public void isVulnerable_noLogRecordFetched_returnsFalse() throws IOException { MockWebServer mockWebServer = new MockWebServer(); mockWebServer.enqueue( new MockResponse().setResponseCode(HttpStatus.NOT_FOUND.code()).setBody("")); client = new TcsClient(VALID_DOMAIN, VALID_PORT, mockWebServer.url("/").toString(), httpClient); boolean detectionResult = client.hasOobLog(SECRET); assertThat(detectionResult).isFalse(); mockWebServer.shutdown(); } @Test public void isVulnerable_requestFailed_returnsFalse() { client = new TcsClient(VALID_DOMAIN, VALID_PORT, "http://unknownhost/path", httpClient); boolean detectionResult = client.hasOobLog(SECRET); assertThat(detectionResult).isFalse(); } } ================================================ FILE: plugin/src/test/java/com/google/tsunami/plugin/payload/PayloadGeneratorModuleTest.java ================================================ /* * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.plugin.payload; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import com.google.common.collect.ImmutableList; import com.google.inject.Guice; import com.google.protobuf.BoolValue; import com.google.protobuf.StringValue; import com.google.tsunami.common.net.http.HttpClient; import com.google.tsunami.common.net.http.HttpClientModule; import com.google.tsunami.plugin.TcsClient; import com.google.tsunami.plugin.TcsClientCliOptions; import com.google.tsunami.plugin.TcsConfigProperties; import com.google.tsunami.proto.PayloadDefinition; import com.google.tsunami.proto.PayloadGeneratorConfig.ExecutionEnvironment; import com.google.tsunami.proto.PayloadGeneratorConfig.InterpretationEnvironment; import com.google.tsunami.proto.PayloadGeneratorConfig.VulnerabilityType; import com.google.tsunami.proto.PayloadValidationType; import java.io.IOException; import java.security.SecureRandom; import java.util.Arrays; import java.util.List; import javax.inject.Inject; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameter; import org.junit.runners.Parameterized.Parameters; /** Tests for {@link PayloadGeneratorModule} */ @RunWith(Parameterized.class) public final class PayloadGeneratorModuleTest { private static final String DOMAIN_1 = "mydomain1.com"; private static final Integer PORT_1 = 1111; private static final String POLLING_URI_1 = String.format("http://%s:%d", DOMAIN_1, PORT_1); private static final String DOMAIN_2 = "http://mydomain2.com:2222"; private static final Integer PORT_2 = 2222; private static final String POLLING_URI_2 = String.format("http://%s:%d", DOMAIN_2, PORT_2); @Inject private HttpClient httpClient; private TcsClientCliOptions cliOptions; private TcsConfigProperties configProperties; private PayloadGeneratorModule module; private final PayloadDefinition.Builder goodCallbackDefinition = PayloadDefinition.newBuilder() .setName(StringValue.of("Test 1")) .setInterpretationEnvironment(InterpretationEnvironment.LINUX_SHELL) .setExecutionEnvironment(ExecutionEnvironment.EXEC_INTERPRETATION_ENVIRONMENT) .addVulnerabilityType(VulnerabilityType.REFLECTIVE_RCE) .setUsesCallbackServer(BoolValue.of(true)) .setPayloadString(StringValue.of("curl $TSUNAMI_PAYLOAD_TOKEN_URL")); private final PayloadDefinition.Builder goodNoCallbackDefinition = PayloadDefinition.newBuilder() .setName(StringValue.of("Test 2")) .setInterpretationEnvironment(InterpretationEnvironment.LINUX_SHELL) .setExecutionEnvironment(ExecutionEnvironment.EXEC_INTERPRETATION_ENVIRONMENT) .addVulnerabilityType(VulnerabilityType.REFLECTIVE_RCE) .setUsesCallbackServer(BoolValue.of(false)) .setPayloadString(StringValue.of("my payload")) .setValidationType(PayloadValidationType.VALIDATION_REGEX) .setValidationRegex(StringValue.of("myregex")); @Before public void setUp() { Guice.createInjector(new HttpClientModule.Builder().build()).injectMembers(this); this.module = new PayloadGeneratorModule(new SecureRandom()); cliOptions = new TcsClientCliOptions(); configProperties = new TcsConfigProperties(); } @Test public void providesTcsClient_withNoConfig_returnsInvalidTcsClient() { TcsClient client = module.providesTcsClient(null, null, null, httpClient); assertFalse(client.isCallbackServerEnabled()); } @Test public void providesTcsClient_withGoodConfig_returnsValidTcsClient() { TcsClient client = module.providesTcsClient(DOMAIN_1, PORT_1, POLLING_URI_1, httpClient); assertTrue(client.isCallbackServerEnabled()); } @Test public void providesTcsClient_withConfigPropertiesAndCliOptions_prioritizesCliOptions() { configProperties.callbackAddress = DOMAIN_2; configProperties.callbackPort = PORT_2; configProperties.pollingUri = POLLING_URI_2; cliOptions.callbackAddress = DOMAIN_1; cliOptions.callbackPort = PORT_1; cliOptions.pollingUri = POLLING_URI_1; String callbackAddress = module.providesCallbackAddress(configProperties, cliOptions); Integer callbackPort = module.providesCallbackPort(configProperties, cliOptions); String pollingUri = module.providesCallbackPollingUri(configProperties, cliOptions); assertThat(callbackAddress).isEqualTo(DOMAIN_1); assertThat(callbackPort).isEqualTo(PORT_1); assertThat(pollingUri).isEqualTo(POLLING_URI_1); } @Parameter(0) public String callbackAddress; @Parameter(1) public Integer callbackPort; @Parameter(2) public String pollingUri; @Parameter(3) public Class exceptionClass; @Parameters public static List data() { return Arrays.asList( new Object[][] { {null, 1, "mydomain.com", NullPointerException.class}, {"mydomain.com", null, "mydomain.com", NullPointerException.class}, {"mydomain.com", 1, null, NullPointerException.class}, {"mydomain.com", 0, "mydomain.com", IllegalArgumentException.class}, {"a bad address", 1, "mydomain.com", IllegalArgumentException.class}, }); } @Test public void providesTcsClient_withBadConfig_throwsException() { assertThrows( this.exceptionClass, () -> module.providesTcsClient( this.callbackAddress, this.callbackPort, this.pollingUri, httpClient)); } @Test public void provideParsedPayloads_returnsSomePayloads() throws IOException { ImmutableList payloads = module.provideParsedPayloads(); assertThat(payloads).isNotEmpty(); } @Test public void validatePayloads_withGoodPayloads_returnsPayloads() throws IOException { PayloadDefinition p0 = goodCallbackDefinition.build(); PayloadDefinition p1 = goodNoCallbackDefinition.build(); ImmutableList payloads = module.validatePayloads(ImmutableList.of(p0, p1)); assertThat(payloads).containsExactly(p0, p1).inOrder(); } @Test public void validatePayloads_withoutInterpretationEnvironment_throwsException() throws IOException { PayloadDefinition p = goodCallbackDefinition.clearInterpretationEnvironment().build(); Throwable thrown = assertThrows( IllegalArgumentException.class, () -> module.validatePayloads(ImmutableList.of(p))); assertThat(thrown).hasMessageThat().contains("interpretation_environment"); } @Test public void validatePayloads_withoutExecutionEnvironment_throwsException() throws IOException { PayloadDefinition p = goodCallbackDefinition.clearExecutionEnvironment().build(); Throwable thrown = assertThrows( IllegalArgumentException.class, () -> module.validatePayloads(ImmutableList.of(p))); assertThat(thrown).hasMessageThat().contains("exeuction_environment"); } @Test public void validatePayloads_withoutVulnerabilityType_throwsException() throws IOException { PayloadDefinition p = goodCallbackDefinition.clearVulnerabilityType().build(); Throwable thrown = assertThrows( IllegalArgumentException.class, () -> module.validatePayloads(ImmutableList.of(p))); assertThat(thrown).hasMessageThat().contains("vulnerability_type"); } @Test public void validatePayloads_withoutPayloadString_throwsException() throws IOException { PayloadDefinition p = goodCallbackDefinition.clearPayloadString().build(); Throwable thrown = assertThrows( IllegalArgumentException.class, () -> module.validatePayloads(ImmutableList.of(p))); assertThat(thrown).hasMessageThat().contains("payload_string"); } @Test public void validatePayloads_withCallbackPayloadWithoutUrlToken_throwsException() throws IOException { PayloadDefinition p = goodCallbackDefinition.setPayloadString(StringValue.of("my payload")).build(); Throwable thrown = assertThrows( IllegalArgumentException.class, () -> module.validatePayloads(ImmutableList.of(p))); assertThat(thrown).hasMessageThat().contains("$TSUNAMI_PAYLOAD_TOKEN_URL"); } @Test public void validatePayloads_withNoCallbackPayloadWithoutValidationType_throwsException() throws IOException { PayloadDefinition p = goodNoCallbackDefinition.clearValidationType().build(); Throwable thrown = assertThrows( IllegalArgumentException.class, () -> module.validatePayloads(ImmutableList.of(p))); assertThat(thrown).hasMessageThat().contains("validation_type"); } @Test public void validatePayloads_withRegexValidationWithoutValidationRegex_throwsException() throws IOException { PayloadDefinition p = goodNoCallbackDefinition.clearValidationRegex().build(); Throwable thrown = assertThrows( IllegalArgumentException.class, () -> module.validatePayloads(ImmutableList.of(p))); assertThat(thrown).hasMessageThat().contains("validation_regex"); } } ================================================ FILE: plugin/src/test/java/com/google/tsunami/plugin/payload/PayloadGeneratorWithCallbackServerTest.java ================================================ /* * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.plugin.payload; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import com.google.inject.Guice; import com.google.tsunami.common.net.http.HttpClientModule; import com.google.tsunami.plugin.payload.testing.FakePayloadGeneratorModule; import com.google.tsunami.plugin.payload.testing.PayloadTestHelper; import com.google.tsunami.proto.PayloadGeneratorConfig; import java.io.IOException; import java.security.SecureRandom; import java.util.Arrays; import javax.inject.Inject; import okhttp3.mockwebserver.MockWebServer; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** Tests for {@link PayloadGenerator} for cases which utilize the callback server. */ @RunWith(JUnit4.class) public final class PayloadGeneratorWithCallbackServerTest { @Inject private PayloadGenerator payloadGenerator; private MockWebServer mockCallbackServer; private final SecureRandom testSecureRandom = new SecureRandom() { @Override public void nextBytes(byte[] bytes) { Arrays.fill(bytes, (byte) 0xFF); } }; private static final PayloadGeneratorConfig LINUX_REFLECTIVE_RCE_CONFIG = PayloadGeneratorConfig.newBuilder() .setVulnerabilityType(PayloadGeneratorConfig.VulnerabilityType.REFLECTIVE_RCE) .setInterpretationEnvironment( PayloadGeneratorConfig.InterpretationEnvironment.LINUX_SHELL) .setExecutionEnvironment( PayloadGeneratorConfig.ExecutionEnvironment.EXEC_INTERPRETATION_ENVIRONMENT) .build(); private static final PayloadGeneratorConfig LINUX_ARBITRARY_FILE_WRITE_CRON_CONFIG = PayloadGeneratorConfig.newBuilder() .setVulnerabilityType(PayloadGeneratorConfig.VulnerabilityType.ARBITRARY_FILE_WRITE) .setInterpretationEnvironment( PayloadGeneratorConfig.InterpretationEnvironment.LINUX_ROOT_CRONTAB) .setExecutionEnvironment( PayloadGeneratorConfig.ExecutionEnvironment.EXEC_INTERPRETATION_ENVIRONMENT) .build(); private static final PayloadGeneratorConfig LINUX_BLIND_RCE_FILE_READ_CONFIG = PayloadGeneratorConfig.newBuilder() .setVulnerabilityType(PayloadGeneratorConfig.VulnerabilityType.BLIND_RCE_FILE_READ) .setInterpretationEnvironment( PayloadGeneratorConfig.InterpretationEnvironment.LINUX_SHELL) .setExecutionEnvironment( PayloadGeneratorConfig.ExecutionEnvironment.EXEC_INTERPRETATION_ENVIRONMENT) .build(); private static final PayloadGeneratorConfig WINDOWS_REFLECTIVE_RCE_CONFIG = PayloadGeneratorConfig.newBuilder() .setVulnerabilityType(PayloadGeneratorConfig.VulnerabilityType.REFLECTIVE_RCE) .setInterpretationEnvironment( PayloadGeneratorConfig.InterpretationEnvironment.WINDOWS_SHELL) .setExecutionEnvironment( PayloadGeneratorConfig.ExecutionEnvironment.EXEC_INTERPRETATION_ENVIRONMENT) .build(); private static final PayloadGeneratorConfig ANY_SSRF_CONFIG = PayloadGeneratorConfig.newBuilder() .setVulnerabilityType(PayloadGeneratorConfig.VulnerabilityType.SSRF) .setInterpretationEnvironment( PayloadGeneratorConfig.InterpretationEnvironment.INTERPRETATION_ANY) .setExecutionEnvironment(PayloadGeneratorConfig.ExecutionEnvironment.EXEC_ANY) .build(); private static final String CORRECT_PRINTF = "printf %s%s%s TSUNAMI_PAYLOAD_START ffffffffffffffff TSUNAMI_PAYLOAD_END"; private static final String CORRECT_CURL_TRACE = "curl --trace /tmp/tsunami-rce -- tsunami-rce-ffffffffffffffff"; private static final String CORRECT_WINDOWS_ECHO = "powershell -Command \"echo TSUNAMI_PAYLOAD_START$(echo" + " ffffffffffffffff)TSUNAMI_PAYLOAD_END\""; @Before public void setUp() throws IOException { mockCallbackServer = new MockWebServer(); mockCallbackServer.start(); Guice.createInjector( new HttpClientModule.Builder().build(), FakePayloadGeneratorModule.builder() .setCallbackServer(mockCallbackServer) .setSecureRng(testSecureRandom) .build()) .injectMembers(this); } @Test public void isCallbackServerEnabled_returnsTrue() { assertTrue(payloadGenerator.isCallbackServerEnabled()); } @Test public void generate_withLinuxConfiguration_returnsCurlPayload() { Payload payload = payloadGenerator.generate(LINUX_REFLECTIVE_RCE_CONFIG); assertThat(payload.getPayload()).contains("curl"); assertThat(payload.getPayload()).contains(mockCallbackServer.getHostName()); assertThat(payload.getPayload()).contains(Integer.toString(mockCallbackServer.getPort(), 10)); assertTrue(payload.getPayloadAttributes().getUsesCallbackServer()); } @Test public void generate_withLinuxConfiguration_returnsPrintfPayload() { Payload payload = payloadGenerator.generateNoCallback(LINUX_REFLECTIVE_RCE_CONFIG); assertThat(payload.getPayload()).isEqualTo(CORRECT_PRINTF); assertFalse(payload.getPayloadAttributes().getUsesCallbackServer()); } @Test public void checkIfExecuted_withLinuxConfiguration_andExecutedCallbackUrl_returnsTrue() throws IOException { mockCallbackServer.enqueue(PayloadTestHelper.generateMockSuccessfulCallbackResponse()); Payload payload = payloadGenerator.generate(LINUX_REFLECTIVE_RCE_CONFIG); assertTrue(payload.checkIfExecuted()); } @Test public void checkIfExecuted_withLinuxConfiguration_andNotExecutedCallbackUrl_returnsFalse() { mockCallbackServer.enqueue(PayloadTestHelper.generateMockUnsuccessfulCallbackResponse()); Payload payload = payloadGenerator.generate(LINUX_REFLECTIVE_RCE_CONFIG); assertFalse(payload.checkIfExecuted()); } @Test public void generate_withCrontabConfiguration_returnsCronCurlPayload() { Payload payload = payloadGenerator.generate(LINUX_ARBITRARY_FILE_WRITE_CRON_CONFIG); assertThat(payload.getPayload()).contains("* * * * * root curl"); assertThat(payload.getPayload()).contains(mockCallbackServer.getHostName()); assertThat(payload.getPayload()).contains(Integer.toString(mockCallbackServer.getPort(), 10)); assertTrue(payload.getPayloadAttributes().getUsesCallbackServer()); } @Test public void checkIfExecuted_withCrontabConfiguration_andExecutedCallbackUrl_returnsTrue() throws IOException { mockCallbackServer.enqueue(PayloadTestHelper.generateMockSuccessfulCallbackResponse()); Payload payload = payloadGenerator.generate(LINUX_ARBITRARY_FILE_WRITE_CRON_CONFIG); assertTrue(payload.checkIfExecuted()); } @Test public void checkIfExecuted_withCrontabConfiguration_andNotExecutedCallbackUrl_returnsFalse() { mockCallbackServer.enqueue(PayloadTestHelper.generateMockUnsuccessfulCallbackResponse()); Payload payload = payloadGenerator.generate(LINUX_ARBITRARY_FILE_WRITE_CRON_CONFIG); assertFalse(payload.checkIfExecuted()); } @Test public void generate_withCurlTraceConfiguration_returnsCurlTracePayload() { Payload payload = payloadGenerator.generateNoCallback(LINUX_BLIND_RCE_FILE_READ_CONFIG); assertThat(payload.getPayload()).isEqualTo(CORRECT_CURL_TRACE); assertFalse(payload.getPayloadAttributes().getUsesCallbackServer()); } @Test public void generate_withWindowsConfiguration_returnsEchoPayload() { Payload payload = payloadGenerator.generateNoCallback(WINDOWS_REFLECTIVE_RCE_CONFIG); assertThat(payload.getPayload()).isEqualTo(CORRECT_WINDOWS_ECHO); assertFalse(payload.getPayloadAttributes().getUsesCallbackServer()); } @Test public void checkIfExecuted_withWindowsConfiguration_andExecutedCallbackUrl_returnsTrue() throws IOException { mockCallbackServer.enqueue(PayloadTestHelper.generateMockSuccessfulCallbackResponse()); Payload payload = payloadGenerator.generate(WINDOWS_REFLECTIVE_RCE_CONFIG); assertTrue(payload.checkIfExecuted()); } @Test public void checkIfExecuted_withWindowsConfiguration_andNotExecutedCallbackUrl_returnsFalse() { mockCallbackServer.enqueue(PayloadTestHelper.generateMockUnsuccessfulCallbackResponse()); Payload payload = payloadGenerator.generate(WINDOWS_REFLECTIVE_RCE_CONFIG); assertFalse(payload.checkIfExecuted()); } @Test public void getPayload_withSsrfConfiguration_returnsCallbackUrl() { Payload payload = payloadGenerator.generate(ANY_SSRF_CONFIG); assertTrue(payload.getPayloadAttributes().getUsesCallbackServer()); assertThat(payload.getPayload()).contains(mockCallbackServer.getHostName()); assertThat(payload.getPayload()).contains(Integer.toString(mockCallbackServer.getPort(), 10)); } @Test public void checkIfExecuted_withSsrfConfiguration_andExecutedUrl_returnsTrue() throws IOException { mockCallbackServer.enqueue(PayloadTestHelper.generateMockSuccessfulCallbackResponse()); Payload payload = payloadGenerator.generate(ANY_SSRF_CONFIG); assertTrue(payload.checkIfExecuted()); } @Test public void getPayload_withSsrfConfiguration_andNotExecutedUrl_returnsFalse() { mockCallbackServer.enqueue(PayloadTestHelper.generateMockUnsuccessfulCallbackResponse()); Payload payload = payloadGenerator.generate(ANY_SSRF_CONFIG); assertFalse(payload.checkIfExecuted()); } @Test public void generate_withoutVulnerabilityType_throwsNotImplementedException() { assertThrows( NotImplementedException.class, () -> payloadGenerator.generate( PayloadGeneratorConfig.newBuilder() .setInterpretationEnvironment( PayloadGeneratorConfig.InterpretationEnvironment.LINUX_SHELL) .setExecutionEnvironment( PayloadGeneratorConfig.ExecutionEnvironment.EXEC_INTERPRETATION_ENVIRONMENT) .build())); } @Test public void generate_withoutInterpretationEnvironment_throwsNotImplementedException() { assertThrows( NotImplementedException.class, () -> payloadGenerator.generate( PayloadGeneratorConfig.newBuilder() .setVulnerabilityType(PayloadGeneratorConfig.VulnerabilityType.REFLECTIVE_RCE) .setExecutionEnvironment( PayloadGeneratorConfig.ExecutionEnvironment.EXEC_INTERPRETATION_ENVIRONMENT) .build())); } @Test public void generate_withoutExecutionEnvironment_throwsNotImplementedException() { assertThrows( NotImplementedException.class, () -> payloadGenerator.generate( PayloadGeneratorConfig.newBuilder() .setVulnerabilityType(PayloadGeneratorConfig.VulnerabilityType.REFLECTIVE_RCE) .setInterpretationEnvironment( PayloadGeneratorConfig.InterpretationEnvironment.LINUX_SHELL) .build())); } @Test public void generate_withoutConfig_throwsNotImplementedException() { assertThrows( NotImplementedException.class, () -> payloadGenerator.generate(PayloadGeneratorConfig.getDefaultInstance())); } } ================================================ FILE: plugin/src/test/java/com/google/tsunami/plugin/payload/PayloadGeneratorWithoutCallbackServerTest.java ================================================ /* * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.plugin.payload; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import com.google.inject.Guice; import com.google.protobuf.ByteString; import com.google.tsunami.common.net.http.HttpClientModule; import com.google.tsunami.plugin.payload.testing.FakePayloadGeneratorModule; import com.google.tsunami.proto.PayloadGeneratorConfig; import java.security.SecureRandom; import java.util.Arrays; import javax.inject.Inject; import okhttp3.mockwebserver.MockWebServer; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** Tests for {@link PayloadGenerator} for cases which do not utilize the callback server. */ @RunWith(JUnit4.class) public final class PayloadGeneratorWithoutCallbackServerTest { @Inject private PayloadGenerator payloadGenerator; private MockWebServer mockCallbackServer; private final SecureRandom testSecureRandom = new SecureRandom() { @Override public void nextBytes(byte[] bytes) { Arrays.fill(bytes, (byte) 0xFF); } }; private static final PayloadGeneratorConfig LINUX_REFLECTIVE_RCE_CONFIG = PayloadGeneratorConfig.newBuilder() .setVulnerabilityType(PayloadGeneratorConfig.VulnerabilityType.REFLECTIVE_RCE) .setInterpretationEnvironment( PayloadGeneratorConfig.InterpretationEnvironment.LINUX_SHELL) .setExecutionEnvironment( PayloadGeneratorConfig.ExecutionEnvironment.EXEC_INTERPRETATION_ENVIRONMENT) .build(); private static final PayloadGeneratorConfig LINUX_ARBITRARY_FILE_WRITE_CRON_CONFIG = PayloadGeneratorConfig.newBuilder() .setVulnerabilityType(PayloadGeneratorConfig.VulnerabilityType.ARBITRARY_FILE_WRITE) .setInterpretationEnvironment( PayloadGeneratorConfig.InterpretationEnvironment.LINUX_ROOT_CRONTAB) .setExecutionEnvironment( PayloadGeneratorConfig.ExecutionEnvironment.EXEC_INTERPRETATION_ENVIRONMENT) .build(); private static final PayloadGeneratorConfig LINUX_BLIND_RCE_FILE_READ_CONFIG = PayloadGeneratorConfig.newBuilder() .setVulnerabilityType(PayloadGeneratorConfig.VulnerabilityType.BLIND_RCE_FILE_READ) .setInterpretationEnvironment( PayloadGeneratorConfig.InterpretationEnvironment.LINUX_SHELL) .setExecutionEnvironment( PayloadGeneratorConfig.ExecutionEnvironment.EXEC_INTERPRETATION_ENVIRONMENT) .build(); private static final PayloadGeneratorConfig JAVA_REFLECTIVE_RCE_CONFIG = PayloadGeneratorConfig.newBuilder() .setVulnerabilityType(PayloadGeneratorConfig.VulnerabilityType.REFLECTIVE_RCE) .setInterpretationEnvironment(PayloadGeneratorConfig.InterpretationEnvironment.JAVA) .setExecutionEnvironment( PayloadGeneratorConfig.ExecutionEnvironment.EXEC_INTERPRETATION_ENVIRONMENT) .build(); private static final PayloadGeneratorConfig JSP_REFLECTIVE_RCE_CONFIG = PayloadGeneratorConfig.newBuilder() .setVulnerabilityType(PayloadGeneratorConfig.VulnerabilityType.REFLECTIVE_RCE) .setInterpretationEnvironment(PayloadGeneratorConfig.InterpretationEnvironment.JSP) .setExecutionEnvironment( PayloadGeneratorConfig.ExecutionEnvironment.EXEC_INTERPRETATION_ENVIRONMENT) .build(); private static final PayloadGeneratorConfig WINDOWS_REFLECTIVE_RCE_CONFIG = PayloadGeneratorConfig.newBuilder() .setVulnerabilityType(PayloadGeneratorConfig.VulnerabilityType.REFLECTIVE_RCE) .setInterpretationEnvironment( PayloadGeneratorConfig.InterpretationEnvironment.WINDOWS_SHELL) .setExecutionEnvironment( PayloadGeneratorConfig.ExecutionEnvironment.EXEC_INTERPRETATION_ENVIRONMENT) .build(); private static final PayloadGeneratorConfig ANY_SSRF_CONFIG = PayloadGeneratorConfig.newBuilder() .setVulnerabilityType(PayloadGeneratorConfig.VulnerabilityType.SSRF) .setInterpretationEnvironment( PayloadGeneratorConfig.InterpretationEnvironment.INTERPRETATION_ANY) .setExecutionEnvironment(PayloadGeneratorConfig.ExecutionEnvironment.EXEC_ANY) .build(); private static final String CORRECT_PRINTF = "printf %s%s%s TSUNAMI_PAYLOAD_START ffffffffffffffff TSUNAMI_PAYLOAD_END"; private static final String CORRECT_CURL_TRACE = "curl --trace /tmp/tsunami-rce -- tsunami-rce-ffffffffffffffff"; private static final String CORRECT_WINDOWS_ECHO = "powershell -Command \"echo TSUNAMI_PAYLOAD_START$(echo" + " ffffffffffffffff)TSUNAMI_PAYLOAD_END\""; @Before public void setUp() { Guice.createInjector( new HttpClientModule.Builder().build(), FakePayloadGeneratorModule.builder().setSecureRng(testSecureRandom).build()) .injectMembers(this); } @Test public void isCallbackServerEnabled_returnsFalse() { assertFalse(payloadGenerator.isCallbackServerEnabled()); } @Test public void getNonCallbackPayload_withLinuxConfiguration_returnsPrintfPayload() { Payload payload = payloadGenerator.generateNoCallback(LINUX_REFLECTIVE_RCE_CONFIG); assertThat(payload.getPayload()).isEqualTo(CORRECT_PRINTF); assertFalse(payload.getPayloadAttributes().getUsesCallbackServer()); } @Test public void getPayload_withLinuxConfiguration_returnsPrintfPayload() { Payload payload = payloadGenerator.generate(LINUX_REFLECTIVE_RCE_CONFIG); assertThat(payload.getPayload()).isEqualTo(CORRECT_PRINTF); assertFalse(payload.getPayloadAttributes().getUsesCallbackServer()); } @Test public void checkIfExecuted_withLinuxConfiguration_andCorrectInput_returnsTrue() { Payload payload = payloadGenerator.generate(LINUX_REFLECTIVE_RCE_CONFIG); assertTrue( payload.checkIfExecuted( ByteString.copyFromUtf8( "RANDOMOUTPUTTSUNAMI_PAYLOAD_STARTffffffffffffffffTSUNAMI_PAYLOAD_END"))); } @Test public void checkIfExecuted_withLinuxConfiguration_andIncorectInput_returnsFalse() { Payload payload = payloadGenerator.generate(LINUX_REFLECTIVE_RCE_CONFIG); assertFalse(payload.checkIfExecuted(ByteString.copyFromUtf8(CORRECT_PRINTF))); } @Test public void generateNonCallbackPayload_withCrontabConfiguration_throwsNotImplementedException() { assertThrows( NotImplementedException.class, () -> payloadGenerator.generateNoCallback(LINUX_ARBITRARY_FILE_WRITE_CRON_CONFIG)); } @Test public void getNonCallbackPayload_withBlindRceReadConfiguration_returnsCurlTracePayload() { Payload payload = payloadGenerator.generateNoCallback(LINUX_BLIND_RCE_FILE_READ_CONFIG); assertThat(payload.getPayload()).isEqualTo(CORRECT_CURL_TRACE); assertFalse(payload.getPayloadAttributes().getUsesCallbackServer()); } @Test public void getPayload_withBlindRceReadConfiguration_returnsCurlTracePayload() { Payload payload = payloadGenerator.generate(LINUX_BLIND_RCE_FILE_READ_CONFIG); assertThat(payload.getPayload()).isEqualTo(CORRECT_CURL_TRACE); assertFalse(payload.getPayloadAttributes().getUsesCallbackServer()); } @Test public void checkIfExecuted_withBlindRceReadConfiguration_andCorrectInput_returnsTrue() { Payload payload = payloadGenerator.generate(LINUX_BLIND_RCE_FILE_READ_CONFIG); assertTrue( payload.checkIfExecuted( ByteString.copyFromUtf8("RANDOMOUTPUTtsunami-rce-ffffffffffffffff"))); } @Test public void checkIfExecuted_withBlindRceReadConfiguration_andIncorectInput_returnsFalse() { Payload payload = payloadGenerator.generate(LINUX_BLIND_RCE_FILE_READ_CONFIG); assertFalse(payload.checkIfExecuted(ByteString.copyFromUtf8("RANDOMINPUT"))); } @Test public void getNonCallbackPayload_withWindowsConfiguration_returnsPrintfPayload() { Payload payload = payloadGenerator.generateNoCallback(WINDOWS_REFLECTIVE_RCE_CONFIG); assertThat(payload.getPayload()).isEqualTo(CORRECT_WINDOWS_ECHO); assertFalse(payload.getPayloadAttributes().getUsesCallbackServer()); } @Test public void getPayload_withWindowsConfiguration_returnsEchoPayload() { Payload payload = payloadGenerator.generate(WINDOWS_REFLECTIVE_RCE_CONFIG); assertThat(payload.getPayload()).isEqualTo(CORRECT_WINDOWS_ECHO); assertFalse(payload.getPayloadAttributes().getUsesCallbackServer()); } @Test public void checkIfExecuted_withWindowsConfiguration_andCorrectInput_returnsTrue() { Payload payload = payloadGenerator.generate(WINDOWS_REFLECTIVE_RCE_CONFIG); assertTrue( payload.checkIfExecuted( ByteString.copyFromUtf8( "RANDOMOUTPUTTSUNAMI_PAYLOAD_STARTffffffffffffffffTSUNAMI_PAYLOAD_END"))); } @Test public void checkIfExecuted_withWindowsConfiguration_andIncorectInput_returnsFalse() { Payload payload = payloadGenerator.generate(WINDOWS_REFLECTIVE_RCE_CONFIG); assertFalse(payload.checkIfExecuted(ByteString.copyFromUtf8(CORRECT_PRINTF))); } @Test public void getPayload_withJavaConfiguration_returnsPrintfPayload() { Payload payload = payloadGenerator.generate(JAVA_REFLECTIVE_RCE_CONFIG); assertThat(payload.getPayload()).isEqualTo( "String.format(\"%s%s%s\", \"TSUNAMI_PAYLOAD_START\", \"ffffffffffffffff\"," + " \"TSUNAMI_PAYLOAD_END\")"); assertFalse(payload.getPayloadAttributes().getUsesCallbackServer()); } @Test public void checkIfExecuted_withJavaConfiguration_andCorrectInput_returnsTrue() { Payload payload = payloadGenerator.generate(JAVA_REFLECTIVE_RCE_CONFIG); assertTrue( payload.checkIfExecuted( ByteString.copyFromUtf8( "RANDOMOUTPUTTSUNAMI_PAYLOAD_STARTffffffffffffffffTSUNAMI_PAYLOAD_END"))); } @Test public void checkIfExecuted_withJavaConfiguration_andIncorrectInput_returnsFalse() { Payload payload = payloadGenerator.generate(JAVA_REFLECTIVE_RCE_CONFIG); assertFalse( payload.checkIfExecuted( ByteString.copyFromUtf8("TSUNAMI_PAYLOAD_START ffffffffffffffff TSUNAMI_PAYLOAD_END"))); } @Test public void getPayload_withJspConfiguration_returnsPrintfPayload() { Payload payload = payloadGenerator.generate(JSP_REFLECTIVE_RCE_CONFIG); assertThat(payload.getPayload()) .isEqualTo( "<% out.print(String.format(\"%s%s%s\",\"TSUNAMI_PAYLOAD_START\", \"ffffffffffffffff\"," + " \"TSUNAMI_PAYLOAD_END\")); %>"); assertFalse(payload.getPayloadAttributes().getUsesCallbackServer()); } @Test public void checkIfExecuted_withJspConfiguration_andCorrectInput_returnsTrue() { Payload payload = payloadGenerator.generate(JSP_REFLECTIVE_RCE_CONFIG); assertTrue( payload.checkIfExecuted( ByteString.copyFromUtf8( "RANDOMOUTPUTTSUNAMI_PAYLOAD_STARTffffffffffffffffTSUNAMI_PAYLOAD_END"))); } @Test public void checkIfExecuted_withJspConfiguration_andIncorrectInput_returnsFalse() { Payload payload = payloadGenerator.generate(JSP_REFLECTIVE_RCE_CONFIG); assertFalse( payload.checkIfExecuted( ByteString.copyFromUtf8("TSUNAMI_PAYLOAD_START ffffffffffffffff TSUNAMI_PAYLOAD_END"))); } @Test public void getPayload_withSsrfConfiguration_returnsGooglePayload() { Payload payload = payloadGenerator.generate(ANY_SSRF_CONFIG); assertThat(payload.getPayload()).isEqualTo("http://public-firing-range.appspot.com/"); assertFalse(payload.getPayloadAttributes().getUsesCallbackServer()); } @Test public void checkIfExecuted_withSsrfConfiguration_andCorrectInput_returnsTrue() { Payload payload = payloadGenerator.generate(ANY_SSRF_CONFIG); assertTrue(payload.checkIfExecuted("

What is the Firing Range?

")); } @Test public void checkIfExecuted_withSsrfConfiguration_andIncorrectInput_returnsFalse() { Payload payload = payloadGenerator.generate(ANY_SSRF_CONFIG); assertFalse(payload.checkIfExecuted("404 not found")); } @Test public void generate_withoutVulnerabilityType_throwsNotImplementedException() { assertThrows( NotImplementedException.class, () -> payloadGenerator.generate( PayloadGeneratorConfig.newBuilder() .setInterpretationEnvironment( PayloadGeneratorConfig.InterpretationEnvironment.LINUX_SHELL) .setExecutionEnvironment( PayloadGeneratorConfig.ExecutionEnvironment.EXEC_INTERPRETATION_ENVIRONMENT) .build())); } @Test public void generate_withoutInterpretationEnvironment_throwsNotImplementedException() { assertThrows( NotImplementedException.class, () -> payloadGenerator.generate( PayloadGeneratorConfig.newBuilder() .setVulnerabilityType(PayloadGeneratorConfig.VulnerabilityType.REFLECTIVE_RCE) .setExecutionEnvironment( PayloadGeneratorConfig.ExecutionEnvironment.EXEC_INTERPRETATION_ENVIRONMENT) .build())); } @Test public void generate_withoutExecutionEnvironment_throwsNotImplementedException() { assertThrows( NotImplementedException.class, () -> payloadGenerator.generate( PayloadGeneratorConfig.newBuilder() .setVulnerabilityType(PayloadGeneratorConfig.VulnerabilityType.REFLECTIVE_RCE) .setInterpretationEnvironment( PayloadGeneratorConfig.InterpretationEnvironment.LINUX_SHELL) .build())); } @Test public void generate_withoutConfig_throwsNotImplementedException() { assertThrows( NotImplementedException.class, () -> payloadGenerator.generate(PayloadGeneratorConfig.getDefaultInstance())); } } ================================================ FILE: plugin/src/test/java/com/google/tsunami/plugin/payload/PayloadSecretGeneratorTest.java ================================================ /* * Copyright 2021 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.plugin.payload; import static com.google.common.truth.Truth.assertThat; import java.security.SecureRandom; import java.util.Arrays; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** Tests for {@link PayloadSecretGenerator}. */ @RunWith(JUnit4.class) public final class PayloadSecretGeneratorTest { private static final SecureRandom TEST_RNG = new SecureRandom() { @Override public void nextBytes(byte[] bytes) { Arrays.fill(bytes, (byte) 0xFF); } }; @Test public void generate_always_generatesExpectedSecretString() { PayloadSecretGenerator secretGenerator = new PayloadSecretGenerator(TEST_RNG); assertThat(secretGenerator.generate(4)).isEqualTo("ffffffff"); } } ================================================ FILE: plugin/src/test/java/com/google/tsunami/plugin/payload/PayloadTest.java ================================================ /* * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.plugin.payload; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import com.google.protobuf.ByteString; import com.google.tsunami.proto.PayloadAttributes; import com.google.tsunami.proto.PayloadGeneratorConfig; import java.util.Optional; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** Tests for {@link Payload} class */ @RunWith(JUnit4.class) public final class PayloadTest { private static final PayloadGeneratorConfig CONFIG = PayloadGeneratorConfig.getDefaultInstance(); private static final PayloadAttributes PAYLOAD_ATTRIBUTES = PayloadAttributes.getDefaultInstance(); @Test public void getPayload_returnsPayloadString() { Validator validator = (unused) -> false; Payload payload = new Payload("my-payload", validator, PAYLOAD_ATTRIBUTES, CONFIG); assertEquals("my-payload", payload.getPayload()); } @Test public void checkIfExecuted_withNoParameter_executesValidator() { TestValidatorIsCalledValidator testValidator = new TestValidatorIsCalledValidator(); Payload payload = new Payload("my-payload", testValidator, PAYLOAD_ATTRIBUTES, CONFIG); payload.checkIfExecuted(); assertTrue(testValidator.wasCalled); } @Test public void checkIfExecuted_withString_executesValidator() { TestValidatorIsCalledValidator testValidator = new TestValidatorIsCalledValidator(); Payload payload = new Payload("my-payload", testValidator, PAYLOAD_ATTRIBUTES, CONFIG); payload.checkIfExecuted("my-input"); assertTrue(testValidator.wasCalled); } @Test public void checkIfExecuted_withByteString_executesValidator() { TestValidatorIsCalledValidator testValidator = new TestValidatorIsCalledValidator(); Payload payload = new Payload("my-payload", testValidator, PAYLOAD_ATTRIBUTES, CONFIG); payload.checkIfExecuted(ByteString.copyFromUtf8("my-input")); assertTrue(testValidator.wasCalled); } @Test public void checkIfExecuted_withOptional_executesValidator() { TestValidatorIsCalledValidator testValidator = new TestValidatorIsCalledValidator(); Payload payload = new Payload("my-payload", testValidator, PAYLOAD_ATTRIBUTES, CONFIG); payload.checkIfExecuted(Optional.empty()); assertTrue(testValidator.wasCalled); } @Test public void getPayloadAttributes_returnsPayloadAttributes() { Validator validator = (unused) -> false; Payload payload = new Payload("my-payload", validator, PAYLOAD_ATTRIBUTES, CONFIG); assertEquals(payload.getPayloadAttributes(), PAYLOAD_ATTRIBUTES); } private static final class TestValidatorIsCalledValidator implements Validator { public boolean wasCalled = false; @Override public boolean isExecuted(Optional input) { wasCalled = true; return false; } } } ================================================ FILE: plugin_server/py/common/data/network_endpoint_utils.py ================================================ """Static utility methods pertaining to network endpoint protocol buffer. For any utility update, please consider if Java's network endpoint utils (common/src/main/java/com/google/tsunami/common/data/NetworkEndpointUtils.java) also needs the modification. """ import ipaddress from typing import Optional import network_pb2 AddressFamily = network_pb2.AddressFamily Hostname = network_pb2.Hostname IpAddress = network_pb2.IpAddress NetworkEndpoint = network_pb2.NetworkEndpoint Type = NetworkEndpoint.Type DEFINITELY_IP_TYPES = [ NetworkEndpoint.Type.IP, NetworkEndpoint.Type.IP_PORT, NetworkEndpoint.Type.IP_HOSTNAME, NetworkEndpoint.Type.IP_HOSTNAME_PORT ] DEFINITELY_HOSTNAME_TYPES = [ NetworkEndpoint.Type.HOSTNAME, NetworkEndpoint.Type.HOSTNAME_PORT, NetworkEndpoint.Type.IP_HOSTNAME, NetworkEndpoint.Type.IP_HOSTNAME_PORT ] DEFINITELY_PORT_TYPES = [ NetworkEndpoint.Type.IP_PORT, NetworkEndpoint.Type.IP_HOSTNAME_PORT, NetworkEndpoint.Type.HOSTNAME_PORT ] MAX_PORT_NUMBER = 65535 def has_ip_address(network_endpoint: NetworkEndpoint) -> bool: return network_endpoint.type in DEFINITELY_IP_TYPES def has_hostname(network_endpoint: NetworkEndpoint) -> bool: return network_endpoint.type in DEFINITELY_HOSTNAME_TYPES def has_port(network_endpoint: NetworkEndpoint) -> bool: return network_endpoint.type in DEFINITELY_PORT_TYPES def is_ipv6_endpoint(network_endpoint: NetworkEndpoint) -> bool: return has_ip_address( network_endpoint ) and network_endpoint.ip_address.address_family == AddressFamily.IPV6 def to_uri_authority(network_endpoint: NetworkEndpoint) -> str: """Converts network endpoint to URI string. Composes URI from the given endpoint information. Hostname takes precedence over IP address as hostname is a stable identifier for a physical thing whereas IP addresses are ephemeral and unstable. Args: network_endpoint: instance of a network endpoint protobuf. Returns: Return None with an unspecified endpoint type. Return URI string with valid endpoint type. For example, various combination of ip address, port, and hostname would generate the below uri: * ip_v4 = "1.2.3.4" -> uri = "1.2.3.4" * ip_v6 = "3ffe::1" -> uri = "[3ffe::1]" * host = "localhost" -> uri = "localhost" * ip_v4 = "1.2.3.4" port = 8888 -> uri = "1.2.3.4:8888" * ip_v6 = "3ffe::1" port = 8888 -> uri = "[3ffe::1]:8888" * host = "localhost" port = 8888 -> uri = "localhost:8888" Raises: ValueError: an error occurred while looking up network endpoint type. """ ip_address = network_endpoint.ip_address.address port = network_endpoint.port.port_number hostname = network_endpoint.hostname.name uri = '' if network_endpoint.type == NetworkEndpoint.Type.TYPE_UNSPECIFIED: raise_invalid_network_endpoint_type(network_endpoint.type) if network_endpoint.type == NetworkEndpoint.Type.IP: uri = ip_to_uri(ip_address) elif network_endpoint.type == NetworkEndpoint.Type.IP_PORT: uri = ip_to_uri(ip_address) + ':' + str(port) elif network_endpoint.type == NetworkEndpoint.Type.IP_HOSTNAME or network_endpoint.type == NetworkEndpoint.Type.HOSTNAME: uri = hostname elif network_endpoint.type == NetworkEndpoint.Type.IP_HOSTNAME_PORT or network_endpoint.type == NetworkEndpoint.Type.HOSTNAME_PORT: uri = hostname + ':' + str(port) return uri def for_ip(ip_address: str) -> NetworkEndpoint: """Convert ip address to network endpoint protobuf.""" network_endpoint = create_with_ip(ip_address) network_endpoint.type = NetworkEndpoint.Type.IP return network_endpoint def for_ip_and_port(ip_address: str, port: int) -> NetworkEndpoint: """Convert ip address and port to network endpoint protobuf.""" validate_port(port) network_endpoint = create_with_ip(ip_address) network_endpoint.type = NetworkEndpoint.Type.IP_PORT network_endpoint.port.port_number = port return network_endpoint def for_hostname(hostname: str) -> NetworkEndpoint: """Convert hostname to network endpoint protobuf.""" validate_hostname(hostname) network_endpoint = NetworkEndpoint() network_endpoint.hostname.name = hostname network_endpoint.type = NetworkEndpoint.Type.HOSTNAME return network_endpoint def for_ip_and_hostname(ip_address: str, hostname: str) -> NetworkEndpoint: """Convert ip address and hostname to network endpoint protobuf.""" network_endpoint = create_with_ip(ip_address) network_endpoint.type = NetworkEndpoint.Type.IP_HOSTNAME network_endpoint.hostname.name = hostname return network_endpoint def for_hostname_and_port(hostname: str, port: int) -> NetworkEndpoint: """Convert hostname and port to network endpoint protobuf.""" validate_port(port) validate_hostname(hostname) network_endpoint = NetworkEndpoint() network_endpoint.hostname.name = hostname network_endpoint.type = NetworkEndpoint.Type.HOSTNAME_PORT network_endpoint.port.port_number = port return network_endpoint def for_ip_hostname_and_port(ip_address: str, hostname: str, port: int) -> NetworkEndpoint: """Convert ip address, hostname and port to network endpoint protobuf.""" validate_port(port) network_endpoint = create_with_ip(ip_address) network_endpoint.type = NetworkEndpoint.Type.IP_HOSTNAME_PORT network_endpoint.hostname.name = hostname network_endpoint.port.port_number = port return network_endpoint def for_network_endpoint_and_port(network_endpoint: NetworkEndpoint, port: int) -> Optional[NetworkEndpoint]: """Create protobuf from endpoint type lookup.""" validate_port(port) if network_endpoint.type == NetworkEndpoint.Type.IP: return for_ip_and_port(network_endpoint.ip_address.address, port) elif network_endpoint.type == NetworkEndpoint.Type.HOSTNAME: return for_hostname_and_port(network_endpoint.hostname.name, port) elif network_endpoint.type == NetworkEndpoint.Type.IP_HOSTNAME: return for_ip_hostname_and_port(network_endpoint.ip_address.address, network_endpoint.hostname.name, port) else: raise_invalid_network_endpoint_type(network_endpoint.type) def create_with_ip(ip_address: str) -> NetworkEndpoint: """Converts ip address to protobuf network endpoint.""" try: ipaddress.ip_address(ip_address) except ValueError as exc: raise ValueError('%s is not an IP address.' % ip_address) from exc return NetworkEndpoint( ip_address=IpAddress( address=ip_address, address_family=address_family(ip_address))) def validate_port(port: int) -> None: if (port < 0 or port > MAX_PORT_NUMBER): raise ValueError('Port out of range. Expected [0, %s].' % MAX_PORT_NUMBER) def validate_hostname(hostname: str) -> None: try: ipaddress.ip_address(hostname) raise Exception("Expected hostname, got IP address '%s'." % hostname) except ValueError: pass def raise_invalid_network_endpoint_type(endpoint_type: Type) -> None: raise ValueError('Invalid network endpoint type: %s.' % NetworkEndpoint.Type.Name(endpoint_type)) def address_family(ip_address: str) -> AddressFamily: try: ipaddress.IPv4Address(ip_address) return AddressFamily.IPV4 except ipaddress.AddressValueError: return AddressFamily.IPV6 def ip_to_uri(ip_address: str) -> str: return ip_address if address_family( ip_address) == AddressFamily.IPV4 else '[%s]' % ip_address ================================================ FILE: plugin_server/py/common/data/network_endpoint_utils_test.py ================================================ """Tests for google3.third_party.java_src.tsunami.plugin_server.py.common.data.network_endpoint_utils.""" from absl.testing import absltest from absl.testing import parameterized from common.data import network_endpoint_utils import network_pb2 AddressFamily = network_pb2.AddressFamily Hostname = network_pb2.Hostname IpAddress = network_pb2.IpAddress NetworkEndpoint = network_pb2.NetworkEndpoint Port = network_pb2.Port _IPV4 = '8.8.8.8' _IPV6 = '2001:0db8:85a3:0000:0000:8a2e:0370:7334' _PORT = 80 _HOSTNAME = 'localhost' def _make_ipv4_endpoint(port=False): network_endpoint = NetworkEndpoint( ip_address=IpAddress(address=_IPV4, address_family=AddressFamily.IPV4), ) return _add_network_type_and_port(network_endpoint, port) def _make_ipv6_endpoint(port=False): network_endpoint = NetworkEndpoint( ip_address=IpAddress(address=_IPV6, address_family=AddressFamily.IPV6), ) return _add_network_type_and_port(network_endpoint, port) def _make_hostname_endpoint(port=False): network_endpoint = NetworkEndpoint( hostname=Hostname(name=_HOSTNAME), ) if port: network_endpoint.port.port_number = _PORT network_endpoint.type = NetworkEndpoint.Type.HOSTNAME_PORT else: network_endpoint.type = NetworkEndpoint.Type.HOSTNAME return network_endpoint def _add_network_type_and_port(network_endpoint, port=False): if port: network_endpoint.port.port_number = _PORT network_endpoint.type = NetworkEndpoint.Type.IP_PORT else: network_endpoint.type = NetworkEndpoint.Type.IP return network_endpoint class NetworkEndpointUtilsTest(parameterized.TestCase): def test_has_hostname(self): self.assertTrue(network_endpoint_utils.has_hostname( _make_hostname_endpoint())) self.assertTrue(network_endpoint_utils.has_hostname( _make_hostname_endpoint(True))) network_endpoint = _make_ipv4_endpoint(True) network_endpoint.hostname.name = _HOSTNAME network_endpoint.type = NetworkEndpoint.Type.IP_HOSTNAME_PORT self.assertTrue(network_endpoint_utils.has_hostname(network_endpoint)) def test_has_port(self): self.assertTrue(network_endpoint_utils.has_port( _make_hostname_endpoint(True))) network_endpoint = _make_ipv4_endpoint(True) network_endpoint.hostname.name = _HOSTNAME network_endpoint.type = NetworkEndpoint.Type.IP_HOSTNAME_PORT self.assertTrue(network_endpoint_utils.has_port(network_endpoint)) @parameterized.named_parameters( ('ipv6', _make_ipv6_endpoint()), ('ipv6_and_port', _make_ipv6_endpoint(True))) def test_is_ipv6_endpoint(self, network_endpoint: NetworkEndpoint): self.assertTrue(network_endpoint_utils.is_ipv6_endpoint(network_endpoint)) @parameterized.named_parameters( ('ipv4', _make_ipv4_endpoint()), ('ipv4_and_port', _make_ipv4_endpoint(True)), ('hostname', _make_hostname_endpoint()), ('hostname_and_port', _make_hostname_endpoint(True))) def test_is_ipv6_endpoint_returns_false(self, network_endpoint: NetworkEndpoint): self.assertFalse(network_endpoint_utils.is_ipv6_endpoint(network_endpoint)) @parameterized.named_parameters( ('ipv4', _make_ipv4_endpoint(), _IPV4), ('ipv4_and_port', _make_ipv4_endpoint(True), '8.8.8.8:80'), ('ipv6', _make_ipv6_endpoint(), '[2001:0db8:85a3:0000:0000:8a2e:0370:7334]'), ('ipv6_and_port', _make_ipv6_endpoint(True), '[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:80'), ('hostname', _make_hostname_endpoint(), _HOSTNAME), ('hostname_and_port', _make_hostname_endpoint(True), 'localhost:80')) def test_to_uri_authority(self, network_endpoint: NetworkEndpoint, uri_authority: str): self.assertEqual(uri_authority, network_endpoint_utils.to_uri_authority(network_endpoint)) @parameterized.named_parameters( ('ipv4', _make_ipv4_endpoint(), NetworkEndpoint.Type.IP_HOSTNAME, _HOSTNAME), ('ipv6_endpoint', _make_ipv6_endpoint(), NetworkEndpoint.Type.IP_HOSTNAME, _HOSTNAME), ('ipv4_and_port', _make_ipv4_endpoint(True), NetworkEndpoint.Type.IP_HOSTNAME_PORT, 'localhost:80'), ('ipv6_and_port', _make_ipv6_endpoint(True), NetworkEndpoint.Type.IP_HOSTNAME_PORT, 'localhost:80')) def test_to_uri_authority_ip_and_host(self, network_endpoint: NetworkEndpoint, endpoint_type: NetworkEndpoint.Type, uri_authority: str): network_endpoint.hostname.name = _HOSTNAME network_endpoint.type = endpoint_type self.assertEqual(uri_authority, network_endpoint_utils.to_uri_authority(network_endpoint)) def test_to_uri_authority_ip_and_host_with_unspecified_type(self): network_endpoint = NetworkEndpoint( type=NetworkEndpoint.Type.TYPE_UNSPECIFIED, ) with self.assertRaises(ValueError) as exc: network_endpoint_utils.to_uri_authority(network_endpoint) self.assertEqual('Invalid network endpoint type: TYPE_UNSPECIFIED.', str(exc.exception)) def test_for_ip_with_ipv4(self): self.assertEqual(_make_ipv4_endpoint(), network_endpoint_utils.for_ip(_IPV4)) def test_for_ip_with_ipv6(self): self.assertEqual(_make_ipv6_endpoint(), network_endpoint_utils.for_ip(_IPV6)) def test_for_ip_and_port_with_ipv4_and_port(self): self.assertEqual(_make_ipv4_endpoint(True), network_endpoint_utils.for_ip_and_port(_IPV4, _PORT)) def test_for_ip_and_port_with_ipv6_and_port(self): self.assertEqual(_make_ipv6_endpoint(True), network_endpoint_utils.for_ip_and_port(_IPV6, _PORT)) def test_for_hostname_with_hostname(self): network_endpoint = _make_hostname_endpoint() self.assertEqual(network_endpoint, network_endpoint_utils.for_hostname(_HOSTNAME)) def test_for_hostname_and_port_with_hostname_and_port(self): network_endpoint = _make_hostname_endpoint(True) self.assertEqual( network_endpoint, network_endpoint_utils.for_hostname_and_port(_HOSTNAME, _PORT)) def test_for_ip_and_hostname_with_ipv4_and_hostname(self): network_endpoint = _make_ipv4_endpoint() network_endpoint.hostname.name = _HOSTNAME network_endpoint.type = NetworkEndpoint.Type.IP_HOSTNAME self.assertEqual( network_endpoint, network_endpoint_utils.for_ip_and_hostname(_IPV4, _HOSTNAME)) def test_for_ip_and_hostname_with_ipv6_and_hostname(self): network_endpoint = _make_ipv6_endpoint() network_endpoint.hostname.name = _HOSTNAME network_endpoint.type = NetworkEndpoint.Type.IP_HOSTNAME self.assertEqual( network_endpoint, network_endpoint_utils.for_ip_and_hostname(_IPV6, _HOSTNAME)) def test_for_ip_hostname_and_port_with_ipv4_hostname_and_port(self): network_endpoint = _make_ipv4_endpoint(True) network_endpoint.hostname.name = _HOSTNAME network_endpoint.type = NetworkEndpoint.Type.IP_HOSTNAME_PORT self.assertEqual( network_endpoint, network_endpoint_utils.for_ip_hostname_and_port(_IPV4, _HOSTNAME, _PORT)) def test_for_ip_hostname_and_port_with_ipv6_hostname_and_port(self): network_endpoint = _make_ipv6_endpoint(True) network_endpoint.hostname.name = _HOSTNAME network_endpoint.type = NetworkEndpoint.Type.IP_HOSTNAME_PORT self.assertEqual( network_endpoint, network_endpoint_utils.for_ip_hostname_and_port(_IPV6, _HOSTNAME, _PORT)) def test_for_ip_hostname_and_port_with_invalid_port(self): with self.assertRaises(ValueError) as exc: network_endpoint_utils.for_ip_hostname_and_port(_IPV6, _HOSTNAME, 65536) self.assertEqual('Port out of range. Expected [0, 65535].', str(exc.exception)) def test_for_network_endpoint_and_port_with_ipv4_and_port(self): network_endpoint = network_endpoint_utils.for_ip(_IPV4) with self.assertRaises(ValueError) as exc: network_endpoint_utils.for_network_endpoint_and_port(network_endpoint, -1) self.assertEqual('Port out of range. Expected [0, 65535].', str(exc.exception)) def test_for_network_endpoint_and_port_with_ipv6_and_port(self): network_endpoint = network_endpoint_utils.for_ip(_IPV6) self.assertEqual( _make_ipv6_endpoint(True), network_endpoint_utils.for_network_endpoint_and_port( network_endpoint, _PORT)) def test_for_network_endpoint_and_port_with_hostname_and_port(self): network_endpoint = network_endpoint_utils.for_hostname(_HOSTNAME) self.assertEqual( _make_hostname_endpoint(True), network_endpoint_utils.for_network_endpoint_and_port( network_endpoint, _PORT)) def test_for_network_endpoint_and_port_with_ip_and_port(self): network_endpoint = _make_ipv4_endpoint() network_endpoint.hostname.name = _HOSTNAME network_endpoint.type = NetworkEndpoint.Type.IP_HOSTNAME ip_hostname_port_endpoint = network_endpoint_utils.for_ip_and_port( _IPV4, _PORT) ip_hostname_port_endpoint.hostname.name = _HOSTNAME ip_hostname_port_endpoint.type = NetworkEndpoint.Type.IP_HOSTNAME_PORT self.assertEqual( ip_hostname_port_endpoint, network_endpoint_utils.for_network_endpoint_and_port( network_endpoint, _PORT)) def test_for_ip_with_invalid_ip(self): with self.assertRaises(ValueError) as exc: network_endpoint_utils.for_ip('123') self.assertEqual('123 is not an IP address.', str(exc.exception)) def test_for_ip_and_port_with_invalid_ip(self): with self.assertRaises(ValueError) as exc: network_endpoint_utils.for_ip_and_port('123', _PORT) self.assertEqual('123 is not an IP address.', str(exc.exception)) def test_for_ip_and_port_with_invalid_port(self): with self.assertRaises(ValueError) as exc: network_endpoint_utils.for_ip_and_port(_IPV4, -1) self.assertEqual('Port out of range. Expected [0, 65535].', str(exc.exception)) with self.assertRaises(ValueError) as exc: network_endpoint_utils.for_ip_and_port(_IPV4, 65536) self.assertEqual('Port out of range. Expected [0, 65535].', str(exc.exception)) def test_for_hostname_with_ip(self): with self.assertRaises(Exception) as exc: network_endpoint_utils.for_hostname(_IPV4) self.assertEqual("Expected hostname, got IP address '%s'." % _IPV4, str(exc.exception)) with self.assertRaises(Exception) as exc: network_endpoint_utils.for_hostname(_IPV6) self.assertEqual("Expected hostname, got IP address '%s'." % _IPV6, str(exc.exception)) def test_for_hostname_and_port_with_invalid_ip(self): with self.assertRaises(Exception) as exc: network_endpoint_utils.for_hostname_and_port(_IPV4, _PORT) self.assertEqual("Expected hostname, got IP address '%s'." % _IPV4, str(exc.exception)) with self.assertRaises(Exception) as exc: network_endpoint_utils.for_hostname_and_port(_IPV6, _PORT) self.assertEqual("Expected hostname, got IP address '%s'." % _IPV6, str(exc.exception)) def test_for_hostname_and_port_with_invalid_port(self): with self.assertRaises(ValueError) as exc: network_endpoint_utils.for_hostname_and_port(_HOSTNAME, -1) self.assertEqual('Port out of range. Expected [0, 65535].', str(exc.exception)) with self.assertRaises(ValueError) as exc: network_endpoint_utils.for_hostname_and_port(_HOSTNAME, 65536) self.assertEqual('Port out of range. Expected [0, 65535].', str(exc.exception)) def test_for_network_endpoint_and_port_with_invalid_endpoint_type(self): with self.assertRaises(ValueError) as exc: network_endpoint_utils.for_network_endpoint_and_port( _make_ipv4_endpoint(True), _PORT) self.assertEqual('Invalid network endpoint type: IP_PORT.', str(exc.exception)) with self.assertRaises(ValueError) as exc: network_endpoint_utils.for_network_endpoint_and_port( _make_hostname_endpoint(True), _PORT) self.assertEqual('Invalid network endpoint type: HOSTNAME_PORT.', str(exc.exception)) def testfor_network_endpoint_and_port_with_invalid_port(self): with self.assertRaises(ValueError) as exc: network_endpoint_utils.for_network_endpoint_and_port( _make_ipv4_endpoint(), -1) self.assertEqual('Port out of range. Expected [0, 65535].', str(exc.exception)) with self.assertRaises(ValueError) as exc: network_endpoint_utils.for_network_endpoint_and_port( _make_hostname_endpoint(), 65536) self.assertEqual('Port out of range. Expected [0, 65535].', str(exc.exception)) if __name__ == '__main__': absltest.main() ================================================ FILE: plugin_server/py/common/data/network_service_utils.py ================================================ """Utilities for handling network_service_pb2 protos. For any utility update, please consider if Java's network service utils (common/src/main/java/com/google/tsunami/common/data/NetworkServiceUtils.java) also needs the modification. """ import socket from typing import Optional import urllib.parse from common.data import network_endpoint_utils import network_pb2 import network_service_pb2 urlparse = urllib.parse.urlparse NetworkService = network_service_pb2.NetworkService ServiceContext = network_service_pb2.ServiceContext WebServiceContext = network_service_pb2.WebServiceContext AddressFamily = network_pb2.AddressFamily Hostname = network_pb2.Hostname IpAddress = network_pb2.IpAddress NetworkEndpoint = network_pb2.NetworkEndpoint Port = network_pb2.Port TransportProtocol = network_pb2.TransportProtocol is_plain_http_by_known_web_service_name = { "http": True, "http-alt": True, "http-proxy": True, "https": False, "radan-http": True, "ssl/http": False, "ssl/https": False, } def is_web_service(network_service: NetworkService) -> bool: return network_service.service_name.lower( ) in is_plain_http_by_known_web_service_name def is_plain_http_service(network_service: NetworkService) -> bool: return is_web_service( network_service) and is_plain_http_by_known_web_service_name.get( network_service.service_name.lower(), False) def get_service_name(network_service: NetworkService) -> str: return network_service.software.name.lower( ) or network_service.service_name.lower() def build_uri_network_service(uri_string: str) -> NetworkService: """Compose network service protobuf from URI. Parses the URI string into host, scheme, port, ip address, and ip address family. Then uses the above information to compose the network endpoint. Args: uri_string: the uri of the endpoint Returns: Network endpoint protobuf Raises: ValueError: if uri is not http or https or if ip address is invalid """ uri = urlparse(uri_string) hostname = uri.hostname scheme = uri.scheme validate_scheme(scheme) port = sanitize_port(uri.port, scheme) address_info = socket.getaddrinfo(hostname, port)[0] ip_address = address_info[4][0] address_family = get_address_family(address_info[0]) network_endpoint = NetworkEndpoint( ip_address=IpAddress( address_family=address_family, address=ip_address, ), type=NetworkEndpoint.Type.IP_HOSTNAME_PORT, hostname=Hostname(name=hostname), port=Port(port_number=port), ) return NetworkService( network_endpoint=network_endpoint, transport_protocol=TransportProtocol.TCP, service_name=scheme, service_context=ServiceContext( web_service_context=WebServiceContext(application_root=uri.path) ) ) def build_web_application_root_url(network_service: NetworkService) -> str: """Build the root url for web application service. Args: network_service: network service protobuf with a valid service defined in the is_plain_http_by_known_web_service_name dict. Returns: The root URL for the web service which always ends with a "/" (i.e., http://localhost:8080/, https://127.1.23.1/pathway/) """ if not is_web_service(network_service): raise ValueError("Invalid network service: %s" % network_service) return build_web_protocol( network_service) + build_web_uri_authority( network_service) + build_web_app_root_path(network_service) def build_web_protocol(network_service: NetworkService) -> str: if is_plain_http_service(network_service): return "http://" else: return "https://" def build_web_uri_authority(network_service: NetworkService) -> str: """Creates URI authority using the network service. The URI authority has 2 components: domain name and port number. Removes the port number from URI authority if the web service uses the standard default port. Port 80 for http/unsecure network and port 443 for https/secure network. Args: network_service: contains the service name and network endpoint needed to construct the URI authority. Returns: The URI authority. For example, various combination of ip address, port, hostname and service would generate the below uri: With services 'http', 'http-alt', 'http-proxy', and 'radan-http: * ip_v4 = "1.2.3.4" port = 80 -> uri_athority= "1.2.3.4" * ip_v6 = "3ffe::1" port = 80 -> uri_athority = "[3ffe::1]" * host = "localhost" port = 80 -> uri_athority = "localhost" * ip_v4 = "1.2.3.4" port = 443 -> uri_athority = "1.2.3.4:443" * ip_v6 = "3ffe::1" port = 8000 -> uri_athority = "[3ffe::1]:8000" * host = "localhost" port = 8888 -> uri_athority = "localhost:8888" With services 'https', 'ssl/http', and 'ssl/https': * ip_v4 = "1.2.3.4" port = 443 -> uri_athority = "1.2.3.4" * ip_v6 = "3ffe::1" port = 443 -> uri_athority = "[3ffe::1]" * host = "localhost" port = 443 -> uri_athority = "localhost" * ip_v4 = "1.2.3.4" port = 8000 -> uri_athority = "1.2.3.4:8000" * ip_v6 = "3ffe::1" port = 80 -> uri_athority = "[3ffe::1]:80" * host = "localhost" port = 8888 -> uri_athority = "localhost:8888" """ uri_authority = network_endpoint_utils.to_uri_authority( network_service.network_endpoint) if is_plain_http_service(network_service) and uri_authority.endswith( ":80"): return uri_authority.replace(":80", "") if not is_plain_http_service( network_service) and uri_authority.endswith(":443"): return uri_authority.replace(":443", "") return uri_authority def build_web_app_root_path(network_service: NetworkService) -> str: if network_service.service_context: root_path = network_service.service_context.web_service_context.application_root else: root_path = "/" if not root_path.startswith("/"): root_path = "/" + root_path if not root_path.endswith("/"): root_path = root_path + "/" return root_path def get_address_family(address_family: socket.AddressFamily) -> AddressFamily: if address_family == socket.AF_INET: return AddressFamily.IPV4 elif address_family == socket.AF_INET6: return AddressFamily.IPV6 else: raise ValueError("Invalid address family: %s" % address_family) def sanitize_port(port: Optional[int], scheme: str) -> int: if isinstance(port, type(None)): return get_port(-1, scheme) return get_port(port, scheme) def get_port(port: int, scheme: str) -> int: if port >= 0: return port return 80 if scheme == "http" else 443 def validate_scheme(scheme: str) -> None: if scheme == "http" or scheme == "https": pass else: raise ValueError( "URI scheme should be one of the following: 'http', 'https'") ================================================ FILE: plugin_server/py/common/data/network_service_utils_test.py ================================================ """Tests for google3.third_party.java_src.tsunami.plugin_server.py.common.data.network_service_utils.""" import socket from unittest import mock import urllib.parse from absl.testing import absltest from absl.testing import parameterized from common.data import network_service_utils import network_pb2 import network_service_pb2 AddressFamily = network_pb2.AddressFamily NetworkEndpoint = network_pb2.NetworkEndpoint NetworkService = network_service_pb2.NetworkService Hostname = network_pb2.Hostname IpAddress = network_pb2.IpAddress Port = network_pb2.Port TransportProtocol = network_pb2.TransportProtocol WebServiceContext = network_service_pb2.WebServiceContext _ROOT = 'i_am_root' _PORT = 8888 class NetworkServiceUtilsTest(parameterized.TestCase): def make_web_service(self, service_name: str): network_service = NetworkService(service_name=service_name,) return network_service def make_ip_port_endpoint(self, port: int = _PORT): network_endpoint = NetworkEndpoint( port=Port(port_number=port), ip_address=IpAddress( address='0.0.0.0', address_family=AddressFamily.IPV4), type=NetworkEndpoint.Type.IP_PORT, ) return network_endpoint def make_web_service_context(self, root: str = _ROOT): web_service_context = WebServiceContext(application_root=root) return web_service_context @parameterized.named_parameters( ('http', 'http'), ('http-alt', 'http-alt'), ('http-proxy', 'http-proxy'), ('https', 'https'), ('ssl/http', 'ssl/http'), ('ssl/https', 'ssl/https'), ('radan-http', 'radan-http'), ('is_case_insensitive', 'HTTPS')) def test_is_web_service(self, service_name: str): network_service = self.make_web_service(service_name) self.assertTrue(network_service_utils.is_web_service(network_service)) def test_is_web_service_with_invalid_web_service(self): network_service = self.make_web_service('ssh') self.assertFalse(network_service_utils.is_web_service(network_service)) @parameterized.named_parameters( ('http', 'http'), ('http-alt', 'http-alt'), ('http-proxy', 'http-proxy'), ('radan-http', 'radan-http')) def test_is_plain_http_service(self, service_name: str): network_service = self.make_web_service(service_name) self.assertTrue( network_service_utils.is_plain_http_service(network_service)) @parameterized.named_parameters( ('https', 'https'), ('ssh', 'ssh'), ('ssl/http', 'ssl/http'), ('ssl/https', 'ssl/https')) def test_is_plain_http_service_with_invalid_service(self, service_name: str): network_service = self.make_web_service(service_name) self.assertFalse( network_service_utils.is_plain_http_service(network_service)) def test_get_service_name_with_software(self): network_service = self.make_web_service('ssh') network_service.network_endpoint.CopyFrom(self.make_ip_port_endpoint()) network_service.software.name = 'Oracle' self.assertEqual('oracle', network_service_utils.get_service_name(network_service)) def test_get_service_name_with_web_service(self): network_service = self.make_web_service('ssh') network_service.network_endpoint.CopyFrom(self.make_ip_port_endpoint()) self.assertEqual('ssh', network_service_utils.get_service_name(network_service)) @parameterized.named_parameters( ('http_without_root', 'http', _PORT, '', 'http://0.0.0.0:8888/'), ('http_with_root_path', 'http', _PORT, _ROOT, 'http://0.0.0.0:8888/i_am_root/'), ('root_path_no_leading_slash', 'http', _PORT, '/i_am_root', 'http://0.0.0.0:8888/i_am_root/'), ('http_on_port_80', 'http', 80, _ROOT, 'http://0.0.0.0/i_am_root/'), ('https_on_port_443', 'https', 443, _ROOT, 'https://0.0.0.0/i_am_root/'), ('https_without_root', 'https', 8000, '', 'https://0.0.0.0:8000/')) def test_build_web_application_root_url(self, service_name: str, port: int, root: str, url: str): network_service = self.make_web_service(service_name) network_service.network_endpoint.CopyFrom(self.make_ip_port_endpoint(port)) network_service.service_context.web_service_context.CopyFrom( self.make_web_service_context(root)) self.assertEqual( url, network_service_utils.build_web_application_root_url(network_service)) def test_build_uri_network_service_with_ipv4(self): network_service = self.make_web_service('https') network_service.transport_protocol = TransportProtocol.TCP network_service.service_context.web_service_context.CopyFrom( self.make_web_service_context('/i_am_root')) address_info = socket.getaddrinfo('localhost', 443)[0] ip_address = address_info[4][0] address_family = network_service_utils.get_address_family(address_info[0]) network_endpoint = NetworkEndpoint( port=Port(port_number=443), ip_address=IpAddress( address=ip_address, address_family=address_family), type=NetworkEndpoint.Type.IP_HOSTNAME_PORT, hostname=Hostname(name='localhost') ) network_service.network_endpoint.CopyFrom(network_endpoint) self.assertEqual( network_service, network_service_utils.build_uri_network_service( 'https://localhost/i_am_root')) @mock.patch.object( socket, 'getaddrinfo', new=mock.MagicMock( return_value=[[socket.AF_INET6, None, None, None, ['::1']]])) def test_build_uri_network_service_with_ipv6(self): network_service = self.make_web_service('http') network_service.transport_protocol = TransportProtocol.TCP network_service.service_context.web_service_context.CopyFrom( self.make_web_service_context('/i_am_root')) network_endpoint = NetworkEndpoint( port=Port(port_number=80), ip_address=IpAddress( address='::1', address_family=AddressFamily.IPV6), type=NetworkEndpoint.Type.IP_HOSTNAME_PORT, hostname=Hostname(name='some_hostname_with_ipv6') ) network_service.network_endpoint.CopyFrom(network_endpoint) self.assertEqual( network_service, network_service_utils.build_uri_network_service( 'http://some_hostname_with_ipv6/i_am_root')) @mock.patch.object( urllib.parse, 'urlparse', new=mock.MagicMock( return_value={ 'hostname': 'some_hostname_with_ipv6', 'scheme': 'http', 'port': 0, 'path': 'i_am_root' })) @mock.patch.object( socket, 'getaddrinfo', new=mock.MagicMock( return_value=[[socket.AF_INET6, None, None, None, ['::1']]])) def test_build_uri_network_service_with_port_number_zero(self): network_service = self.make_web_service('http') network_service.transport_protocol = TransportProtocol.TCP network_service.service_context.web_service_context.CopyFrom( self.make_web_service_context('/i_am_root')) network_endpoint = NetworkEndpoint( port=Port(port_number=0), ip_address=IpAddress( address='::1', address_family=AddressFamily.IPV6), type=NetworkEndpoint.Type.IP_HOSTNAME_PORT, hostname=Hostname(name='some_hostname_with_ipv6') ) network_service.network_endpoint.CopyFrom(network_endpoint) self.assertEqual( network_service, network_service_utils.build_uri_network_service( 'http://some_hostname_with_ipv6:0/i_am_root')) def test_build_uri_network_service_with_invalid_scheme(self): with self.assertRaises(ValueError) as err: network_service_utils.build_uri_network_service( 'http-alt://localhost/i_am_root') self.assertEqual( "URI scheme should be one of the following: 'http', 'https'", str(err.exception)) if __name__ == '__main__': absltest.main() ================================================ FILE: plugin_server/py/common/net/http/host_resolver_http_adapter.py ================================================ """Custom HTTP Adapter to handle host-based routing support for load balancers.""" import socket from typing import Optional from urllib import parse import requests from common.net.http.http_header_fields import HttpHeaderFields class HostResolverHttpAdapter(requests.adapters.HTTPAdapter): """Custom HTTP adapter for proper hostname resolution. When load balancers are used, there is a chance that the hostname does not resolve to the IP address of the vulnerable application. When the hostname does not resolve to the given IP address, the IP address returned by NMAP is prioritized and used in the "netloc" portion of the URL (see parse.urlsplit()). This Adapter also adds the host header of the request package that would have been otherwise omitted by default. Attributes: pool_connections: Number of connection pools to cache. pool_max: Maximum number of connections to save in the pool. """ def __init__(self, pool_connections: int, pool_maxsize: int): super().__init__( pool_connections=pool_connections, pool_maxsize=pool_maxsize ) def _add_host_header( self, request: requests.PreparedRequest, hostname: str ) -> None: """Adds host:port as the host header.""" request.headers[HttpHeaderFields.HOST.value] = hostname def _require_ipv6_brackets(self, ip: str) -> str: """Adds enclosing brackets if IPV6.""" try: socket.inet_pton(socket.AF_INET6, ip) return "[%s]" % ip except OSError: return ip def _resolve(self, hostname: str, ip: Optional[str] = None, port: Optional[int] = None) -> Optional[str]: """Use the hostname if it resolves to the ip, else use the ip address. Args: hostname: Hostname of the target network. This could be the domain name or the IP address. ip: Optional IP address of target network. port: Optional port of target network. Returns: String of the resolved hostname. """ if hostname == ip or not ip or ip in socket.getaddrinfo(hostname, port): return hostname return ip def send( self, request: requests.PreparedRequest, ip: Optional[str] = None, **kwargs ) -> requests.Response: result = parse.urlparse(request.url) self._add_host_header(request, result.netloc) # use local dns resolved_host = self._resolve(result.hostname, ip=ip, port=result.port) if resolved_host != result.hostname: resolved_host = self._require_ipv6_brackets(resolved_host) netloc = result.netloc.lower().replace(result.hostname, resolved_host) request.url = parse.urlunparse(( result.scheme, netloc, result.path, result.params, result.query, result.fragment, )) return super().send(request, **kwargs) ================================================ FILE: plugin_server/py/common/net/http/host_resolver_http_adapter_test.py ================================================ """Tests for google3.third_party.java_src.tsunami.plugin_server.py.common.net.requests_http_client.""" from unittest import mock from absl.testing import absltest from absl.testing import parameterized import requests from common.net.http.host_resolver_http_adapter import HostResolverHttpAdapter from common.net.http.http_header_fields import HttpHeaderFields from common.net.http.http_method import HttpMethod class HostResolverHttpAdapterTest(parameterized.TestCase): @classmethod def setUpClass(cls): super().setUpClass() cls.custom_adapter = HostResolverHttpAdapter(5, 10) def setUp(self): super().setUp() self.addCleanup(mock.patch.stopall) # Mock of requests's HTTPAdapter response = requests.Response() response.status_code = 200 mock.patch.object( requests.adapters.HTTPAdapter, 'send', return_value=response, ).start() # Mock hostname lookup self.mock_getaddrinfo = mock.patch('socket.getaddrinfo').start() @parameterized.named_parameters( ('with_hostname', 'vuln-app.com'), ('with_ipv4', '199.21.82.88'), ( 'with_ipv6', '[2001:0db8:85a3:0000:0000:8a2e:0370:7334]', ), ) def test_send_dispatches_with_host_header(self, host): url = 'http://{}:8080/send'.format(host) request = self._prepare_request(url) self.custom_adapter.send(request) requests.adapters.HTTPAdapter.send.assert_called_with(request) self.assertEqual( request.headers.get(HttpHeaderFields.HOST.value), '{}:8080'.format(host) ) def test_send_without_target_ip_dispatches_default_hostname(self): url = 'http://vuln-app.com:8080/send' request = self._prepare_request(url) self.custom_adapter.send(request) requests.adapters.HTTPAdapter.send.assert_called_with(request) self.assertEqual(request.url, url) def test_send_when_hostname_resolves_to_ip_uses_default_hostname(self): url = 'http://vuln-app.com:8080/send' ip = '199.21.82.88' request = self._prepare_request(url) self.mock_getaddrinfo.return_value = [ip] self.custom_adapter.send(request, ip=ip) requests.adapters.HTTPAdapter.send.assert_called_once_with(request) self.assertEqual( request.headers.get(HttpHeaderFields.HOST.value), 'vuln-app.com:8080' ) self.assertEqual(request.url, url) def test_send_when_hostname_is_the_ip_uses_default_hostname(self): ip = '2001:0db8:85a3:0000:0000:8a2e:0370:7334' url = 'http://[{}]:8080/send'.format(ip) request = self._prepare_request(url) self.custom_adapter.send(request, ip=ip) requests.adapters.HTTPAdapter.send.assert_called_once_with(request) self.assertEqual( request.headers.get(HttpHeaderFields.HOST.value), '[{}]:8080'.format(ip)) self.assertEqual(request.url, url) def test_send_when_hostname_is_case_insensitive(self): url = 'http://vuln-APP.com:8080/send' ip = '199.21.82.88' request = self._prepare_request(url) self.custom_adapter.send(request, ip=ip) requests.adapters.HTTPAdapter.send.assert_called_once_with(request) self.assertEqual( request.headers.get(HttpHeaderFields.HOST.value), 'vuln-APP.com:8080' ) self.assertEqual(request.url, 'http://199.21.82.88:8080/send') def test_send_when_hostname_does_not_resolve_to_ipv4_uses_ipv4(self): url = 'http://vuln-app.com:8080/send' ip = '199.21.82.88' request = self._prepare_request(url) self.mock_getaddrinfo.return_value = ['1.1.1.1'] self.custom_adapter.send(request, ip=ip) requests.adapters.HTTPAdapter.send.assert_called_once_with(request) self.assertEqual( request.headers.get(HttpHeaderFields.HOST.value), 'vuln-app.com:8080' ) self.assertEqual(request.url, 'http://199.21.82.88:8080/send') def test_send_when_hostname_does_not_resolve_to_ipv6_uses_ipv6(self): url = 'http://vuln-app.com:8080/send' ip = '2001:0db8:85a3:0000:0000:8a2e:0370:7334' request = self._prepare_request(url) self.mock_getaddrinfo.return_value = ['1.1.1.1'] self.custom_adapter.send(request, ip=ip) requests.adapters.HTTPAdapter.send.assert_called_once_with(request) self.assertEqual( request.headers.get(HttpHeaderFields.HOST.value), 'vuln-app.com:8080' ) self.assertEqual( request.url, 'http://[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:8080/send', ) def _prepare_request(self, url): request = requests.Request( method=HttpMethod.GET, url=url, data=b'HTML content' ) request = request.prepare() request.url = url return request if __name__ == '__main__': absltest.main() ================================================ FILE: plugin_server/py/common/net/http/http_client.py ================================================ """Base class for HTTP client.""" import abc from typing import Optional from common.data.network_service_utils import NetworkService from common.net.http.http_request import HttpRequest from common.net.http.http_response import HttpResponse class HttpClient(metaclass=abc.ABCMeta): """HTTP client library used for communicating with remote servers. Attributes: allow_redirects: Optional boolean to determine whether requests may be redirected. True by default. log_id: the log id. timeout_sec: Optional float. How long to wait for the server to send data before giving up. verify_ssl: Optional boolean to verify SSL certification. True by default. """ TSUNAMI_USER_AGENT = 'TsunamiSecurityScanner' def __init__(self, log_id: Optional[str] = None, timeout_sec: Optional[float] = None, allow_redirects: Optional[bool] = True, verify_ssl: Optional[bool] = True): self.allow_redirects = allow_redirects self.log_id = log_id self.timeout_sec = timeout_sec self.verify_ssl = verify_ssl @abc.abstractmethod def get_log_id(self) -> str: pass @abc.abstractmethod def send(self, http_request: HttpRequest, network_service: Optional[NetworkService] = None) -> HttpResponse: """Send the HTTP request using this client.""" pass @abc.abstractmethod def send_async( self, http_request: HttpRequest, network_service: Optional[NetworkService] = None) -> HttpResponse: """Send the HTTP request asynchronously.""" pass @abc.abstractmethod def modify(self): """Allows client code to modify the configurations of the HTTP client.""" pass class Builder(metaclass=abc.ABCMeta): """Base builder for implementations of HttpClient.""" @abc.abstractmethod def set_allow_redirects(self, allow_redirects: bool): pass @abc.abstractmethod def set_verify_ssl(self, verify_ssl: bool): pass @abc.abstractmethod def set_log_id(self, log_id: str): pass @abc.abstractmethod def set_timeout_sec(self, timeout_sec: float): pass @abc.abstractmethod def build(self): pass ================================================ FILE: plugin_server/py/common/net/http/http_header_fields.py ================================================ """HTTP header field names.""" import enum class HttpHeaderFields(enum.Enum): """HTTP header field definition from https://guava.dev/releases/snapshot/api/docs/com/google/common/net/HttpHeaders.html. Attributes: list: Get all header fields. get_from_lower: Get header field name from a case insensitive name. """ # HTTP Request and Response header fields CACHE_CONTROL = "Cache-Control" CONTENT_LENGTH = "Content-Length" CONTENT_TYPE = "Content-Type" DATE = "Date" PRAGMA = "Pragma" VIA = "Via" WARNING = "Warning" # HTTP Request header fields ACCEPT = "Accept" ACCEPT_CHARSET = "Accept-Charset" ACCEPT_ENCODING = "Accept-Encoding" ACCEPT_LANGUAGE = "Accept-Language" ACCESS_CONTROL_REQUEST_HEADERS = "Access-Control-Request-Headers" ACCESS_CONTROL_REQUEST_METHOD = "Access-Control-Request-Method" AUTHORIZATION = "Authorization" CONNECTION = "Connection" COOKIE = "Cookie" CROSS_ORIGIN_RESOURCE_POLICY = "Cross-Origin-Resource-Policy" EARLY_DATA = "Early-Data" EXPECT = "Expect" FROM = "From" FORWARDED = "Forwarded" FOLLOW_ONLY_WHEN_PRERENDER_SHOWN = "Follow-Only-When-Prerender-Shown" HOST = "Host" HTTP2_SETTINGS = "HTTP2-Settings" IF_MATCH = "If-Match" IF_MODIFIED_SINCE = "If-Modified-Since" IF_NONE_MATCH = "If-None-Match" IF_RANGE = "If-Range" IF_UNMODIFIED_SINCE = "If-Unmodified-Since" LAST_EVENT_ID = "Last-Event-ID" MAX_FORWARDS = "Max-Forwards" ORIGIN = "Origin" ORIGIN_ISOLATION = "Origin-Isolation" PROXY_AUTHORIZATION = "Proxy-Authorization" RANGE = "Range" REFERER = "Referer" REFERRER_POLICY = "Referrer-Policy" NO_REFERRER = "no-referrer" NO_REFFERER_WHEN_DOWNGRADE = "no-referrer-when-downgrade" SAME_ORIGIN = "same-origin" STRICT_ORIGIN = "strict-origin" ORIGIN_WHEN_CROSS_ORIGIN = "origin-when-cross-origin" STRICT_ORIGIN_WHEN_CROSS_ORIGIN = "strict-origin-when-cross-origin" UNSAFE_URL = "unsafe-url" SERVICE_WORKER = "Service-Worker" TE = "TE" UPGRADE = "Upgrade" UPGRADE_INSECURE_REQUESTS = "Upgrade-Insecure-Requests" USER_AGENT = "User-Agent" # HTTP Response header fields ACCEPT_RANGES = "Accept-Ranges" ACCESS_CONTROL_ALLOW_HEADERS = "Access-Control-Allow-Headers" ACCESS_CONTROL_ALLOW_METHODS = "Access-Control-Allow-Methods" ACCESS_CONTROL_ALLOW_ORIGIN = "Access-Control-Allow-Origin" ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK = "Access-Control-Allow-Private-Network" ACCESS_CONTROL_ALLOW_CREDENTIALS = "Access-Control-Allow-Credentials" ACCESS_CONTROL_EXPOSE_HEADERS = "Access-Control-Expose-Headers" ACCESS_CONTROL_MAX_AGE = "Access-Control-Max-Age" AGE = "Age" ALLOW = "Allow" CONTENT_DISPOSITION = "Content-Disposition" CONTENT_ENCODING = "Content-Encoding" CONTENT_LANGUAGE = "Content-Language" CONTENT_LOCATION = "Content-Location" CONTENT_MD5 = "Content-MD5" CONTENT_RANGE = "Content-Range" CONTENT_SECURITY_POLICY = "Content-Security-Policy" CONTENT_SECURITY_POLICY_REPORT_ONLY = "Content-Security-Policy-Report-Only" X_CONTENT_SECURITY_POLICY = "X-Content-Security-Policy" X_CONTENT_SECURITY_POLICY_REPORT_ONLY = "X-Content-Security-Policy-Report-Only" X_WEBKIT_CSP = "X-WebKit-CSP" X_WEBKIT_CSP_REPORT_ONLY = "X-WebKit-CSP-Report-Only" CROSS_ORIGIN_EMBEDDER_POLICY = "Cross-Origin-Embedder-Policy" CROSS_ORIGIN_EMBEDDER_POLICY_REPORT_ONLY = "Cross-Origin-Embedder-Policy-Report-Only" CROSS_ORIGIN_OPENER_POLICY = "Cross-Origin-Opener-Policy" ETAG = "ETag" EXPIRES = "Expires" LAST_MODIFIED = "Last-Modified" LINK = "Link" LOCATION = "Location" KEEP_ALIVE = "Keep-Alive" NO_VARY_SEARCH = "No-Vary-Search" ORIGIN_TRIAL = "Origin-Trial" P3P = "P3P" PROXY_AUTHENTICATE = "Proxy-Authenticate" REFRESH = "Refresh" REPORT_TO = "Report-To" RETRY_AFTER = "Retry-After" SERVER = "Server" SERVER_TIMING = "Server-Timing" SERVICE_WORKER_ALLOWED = "Service-Worker-Allowed" SET_COOKIE = "Set-Cookie" SET_COOKIE2 = "Set-Cookie2" SOURCE_MAP = "SourceMap" SUPPORTS_LOADING_MODE = "Supports-Loading-Mode" STRICT_TRANSPORT_SECURITY = "Strict-Transport-Security" TIMING_ALLOW_ORIGIN = "Timing-Allow-Origin" TRAILER = "Trailer" TRANSFER_ENCODING = "Transfer-Encoding" VARY = "Vary" WWW_AUTHENTICATE = "WWW-Authenticate" DNT = "DNT" X_CONTENT_TYPE_OPTIONS = "X-Content-Type-Options" X_DEVICE_IP = "X-Device-IP" X_DEVICE_REFERER = "X-Device-Referer" X_DEVICE_ACCEPT_LANGUAGE = "X-Device-Accept-Language" X_DEVICE_REQUESTED_WITH = "X-Device-Requested-With" X_DO_NOT_TRACK = "X-Do-Not-Track" X_FORWARDED_FOR = "X-Forwarded-For" X_FORWARDED_PROTO = "X-Forwarded-Proto" X_FORWARDED_HOST = "X-Forwarded-Host" X_FORWARDED_PORT = "X-Forwarded-Port" X_FRAME_OPTIONS = "X-Frame-Options" X_POWERED_BY = "X-Powered-By" PUBLIC_KEY_PINS = "Public-Key-Pins" PUBLIC_KEY_PINS_REPORT_ONLY = "Public-Key-Pins-Report-Only" X_REQUEST_ID = "X-Request-ID" X_REQUESTED_WITH = "X-Requested-With" X_USER_IP = "X-User-IP" X_DOWNLOAD_OPTIONS = "X-Download-Options" X_XSS_PROTECTION = "X-XSS-Protection" X_DNS_PREFETCH_CONTROL = "X-DNS-Prefetch-Control" PING_FROM = "Ping-From" PING_TO = "Ping-To" PURPOSE = "Purpose" X_PURPOSE = "X-Purpose" X_MOZ = "X-Moz" DEVICE_MEMORY = "Device-Memory" DOWNLINK = "Downlink" ECT = "ECT" RTT = "RTT" SAVE_DATA = "Save-Data" VIEWPORT_WIDTH = "Viewport-Width" WIDTH = "Width" PERMISSIONS_POLICY = "Permissions-Policy" SEC_CH_PREFERS_COLOR_SCHEME = "Sec-CH-Prefers-Color-Scheme" ACCEPT_CH = "Accept-CH" CRITICAL_CH = "Critical-CH" SEC_CH_UA = "Sec-CH-UA" SEC_CH_UA_ARCH = "Sec-CH-UA-Arch" SEC_CH_UA_MODEL = "Sec-CH-UA-Model" SEC_CH_UA_PLATFORM = "Sec-CH-UA-Platform" SEC_CH_UA_PLATFORM_VERSION = "Sec-CH-UA-Platform-Version" SEC_CH_UA_FULL_VERSION_LIST = "Sec-CH-UA-Full-Version-List" SEC_CH_UA_MOBILE = "Sec-CH-UA-Mobile" SEC_CH_UA_WOW64 = "Sec-CH-UA-WoW64" SEC_CH_UA_BITNESS = "Sec-CH-UA-Bitness" SEC_CH_VIEWPORT_WIDTH = "Sec-CH-Viewport-Width" SEC_CH_VIEWPORT_HEIGHT = "Sec-CH-Viewport-Height" SEC_CH_DPR = "Sec-CH-DPR" SEC_FETCH_DEST = "Sec-Fetch-Dest" SEC_FETCH_MODE = "Sec-Fetch-Mode" SEC_FETCH_SITE = "Sec-Fetch-Site" SEC_FETCH_USER = "Sec-Fetch-User" SEC_METADATA = "Sec-Metadata" SEC_TOKEN_BINDING = "Sec-Token-Binding" SEC_PROVIDED_TOKEN_BINDING_ID = "Sec-Provided-Token-Binding-ID" SEC_REFERRED_TOKEN_BINDING_ID = "Sec-Referred-Token-Binding-ID" SEC_WEBSOCKET_ACCEPT = "Sec-WebSocket-Accept" SEC_WEBSOCKET_EXTENSIONS = "Sec-WebSocket-Extensions" SEC_WEBSOCKET_KEY = "Sec-WebSocket-Key" SEC_WEBSOCKET_PROTOCOL = "Sec-WebSocket-Protocol" SEC_WEBSOCKET_VERSION = "Sec-WebSocket-Version" CDN_LOOP = "CDN-Loop" @classmethod def list(cls): return list(map(lambda field: field.value, HttpHeaderFields)) @classmethod def get_from_lower(cls, name): for field in cls: if field.value.lower() == name.lower(): return field.value ================================================ FILE: plugin_server/py/common/net/http/http_header_fields_test.py ================================================ """Tests for google3.third_party.java_src.tsunami.plugin_server.py.common.net.http_headers.""" from absl.testing import absltest from common.net.http import http_header_fields HttpHeaderFields = http_header_fields.HttpHeaderFields class HttpHeaderFieldsTest(absltest.TestCase): def test_list_returns_all_header_fields(self): self.assertEqual(HttpHeaderFields.list().__len__(), 165) def test_get_from_lower_with_existing_field(self): self.assertEqual( HttpHeaderFields.get_from_lower('CDN-LOOP'), HttpHeaderFields.CDN_LOOP.value) def test_get_from_lower_with_unknown_field(self): self.assertIsNone(HttpHeaderFields.get_from_lower('CDN-LAP')) if __name__ == '__main__': absltest.main() ================================================ FILE: plugin_server/py/common/net/http/http_headers.py ================================================ """HTTP headers utility.""" import collections import re from typing import Optional from common.net.http.http_header_fields import HttpHeaderFields class HttpHeaders: """HTTP headers utility class. Please use Builder() to create instances of this class. Attributes: raw_headers: Collection of header fields and values. Each field could have multiple values. """ def __init__(self): self.raw_headers = collections.defaultdict(list) def names(self) -> set[str]: """Get the list of unique header field names.""" return set(self.raw_headers.keys()) def get(self, name: str) -> Optional[str]: """Get the first value for a specified HTTP field name.""" values = self.get_all(name) if not values: return None return values[0] def get_all(self, name: str) -> list[str]: """Get all values for a specified HTTP field name. Values are in the order they were added to the builder. Args: name: header name Returns: List of matched header values. """ if name is None: raise ValueError('Name cannot be None.') values = self.raw_headers.get(name, []) if values: return values canonicalized_name = _canonicalize(name) return self.raw_headers.get(canonicalized_name, []) @classmethod def builder(cls) -> 'Builder': return Builder() class Builder: """Builder class to create HTTP headers object. Attributes: http_headers: Collection of header field names and corresponding values. """ # RFC 2616 section 4.2. # Allow printable graphic characters except for colon HEADER_NAME_MATCHER = '^([!-~])[^:]*$' # No control characters except for horizontal tab and space HEADER_VALUE_MATCHER = '^[^\x00-\x08\x0A-\x1f\x7f]*$' def __init__(self): self.http_headers = HttpHeaders() def build(self) -> HttpHeaders: return self.http_headers def add_header(self, name: str, value: str, canonicalize: bool = True): """Add HTTP header to headers object. Args: name: HTTP header field name value: HTTP header value canonicalize: Optional boolean to normalize header or not. Default is True. Returns: The builder object. Raises: ValueError: If name or value is None. If header name or value pair does not comply with standards. """ if name is None: raise ValueError('Name cannot be None.') if value is None: raise ValueError('Value cannot be None.') if canonicalize: name = self._canonicalize_header_name(name, value) self.http_headers.raw_headers[name].append(value) return self def _canonicalize_header_name(self, name, value) -> str: if not self._is_legal_header_name(name): raise ValueError('Illegal header name %s.' % name) if not self._is_legal_header_value(value): raise ValueError('Illegal header value %s.' % value) return _canonicalize(name) def _is_legal_header_name(self, name: str) -> bool: return bool(re.fullmatch(self.HEADER_NAME_MATCHER, name)) def _is_legal_header_value(self, value: str) -> bool: return bool(re.fullmatch(self.HEADER_VALUE_MATCHER, value)) def _canonicalize(header_name: str) -> str: """Normalize header field name. Args: header_name: An HTTP header field name. Returns: An HttpHeaderField value or the header_name in lowercase. """ try: return HttpHeaderFields(header_name).value except ValueError: return HttpHeaderFields.get_from_lower(header_name) or header_name.lower() ================================================ FILE: plugin_server/py/common/net/http/http_headers_test.py ================================================ """Tests for google3.third_party.java_src.tsunami.plugin_server.py.common.net.http_headers.""" import collections from absl.testing import absltest from absl.testing import parameterized from common.net.http import http_header_fields from common.net.http import http_headers HttpHeaderFields = http_header_fields.HttpHeaderFields class HttpHeadersTest(parameterized.TestCase): def test_builder_add_header_always_puts_in_headers_map(self): headers = http_headers.Builder().add_header('test_h', 'test_v').build() expected = collections.defaultdict(list, {'test_h': ['test_v']}) self.assertEqual(headers.raw_headers, expected) def test_builder_add_header_with_known_header_canonicalizes_header_name(self): field = HttpHeaderFields.ACCEPT_LANGUAGE.value headers = http_headers.Builder().add_header(field.upper(), 'en').build() expected = collections.defaultdict(list, {field: ['en']}) self.assertEqual(headers.raw_headers, expected) def test_builder_add_header_with_enabled_canonicalization(self): field = HttpHeaderFields.ACCEPT_LANGUAGE.value headers = http_headers.Builder().add_header(field, 'en', True).build() expected = collections.defaultdict(list, {field: ['en']}) self.assertEqual(headers.raw_headers, expected) def test_builder_add_header_with_disabled_canonicalization_adds_raw_header( self): headers = http_headers.Builder().add_header('ACCEPT_Language', 'en', False).build() expected = collections.defaultdict(list, {'ACCEPT_Language': ['en']}) self.assertEqual(headers.raw_headers, expected) @parameterized.named_parameters( ('with_no_name', None, 'en', 'Name cannot be None.'), ('with_no_value', HttpHeaderFields.ACCEPT_LANGUAGE.value, None, 'Value cannot be None.'), ('with_illegal_header_name', ':::', 'en', 'Illegal header name :::.'), ('with_illegal_header_value', HttpHeaderFields.ACCEPT_LANGUAGE.value, chr(1), 'Illegal header value %s.' % chr(1)), ) def test_builder_add_header_raises_error(self, field, value, message): with self.assertRaises(ValueError) as exc: http_headers.Builder().add_header(field, value).build() self.assertEqual(message, str(exc.exception)) def test_names_always_returns_all_header_names(self): headers = http_headers.Builder().add_header( HttpHeaderFields.ACCEPT.value, 'application/xml').add_header( HttpHeaderFields.CONTENT_TYPE.value, 'text/html').add_header(HttpHeaderFields.ACCEPT.value, 'image/webp').build() expected = set( [HttpHeaderFields.ACCEPT.value, HttpHeaderFields.CONTENT_TYPE.value]) self.assertEqual(headers.names(), expected) def test_get_when_requested_header_exists_returns_requested_header(self): headers = http_headers.Builder().add_header( HttpHeaderFields.ACCEPT.value, 'application/xml').add_header(HttpHeaderFields.CONTENT_TYPE.value, 'text/html').build() self.assertEqual( headers.get(HttpHeaderFields.ACCEPT.value), 'application/xml') def test_get_when_multiple_values_exist_returns_first_value(self): headers = http_headers.Builder().add_header( HttpHeaderFields.ACCEPT.value, 'application/xml').add_header( HttpHeaderFields.CONTENT_TYPE.value, 'text/html').add_header(HttpHeaderFields.ACCEPT.value, 'image/webp').build() self.assertEqual( headers.get(HttpHeaderFields.ACCEPT.value), 'application/xml') def test_get_when_requested_header_does_not_exist_returns_none(self): headers = http_headers.Builder().add_header(HttpHeaderFields.ACCEPT.value, 'app/xml').build() self.assertIsNone(headers.get('cookie')) def test_get_with_none_header_name_raise_error(self): headers = http_headers.Builder().add_header(HttpHeaderFields.ACCEPT.value, 'app/xml').build() with self.assertRaises(ValueError) as exc: headers.get(None) self.assertEqual('Name cannot be None.', str(exc.exception)) def test_get_all_always_returns_requested_values(self): headers = http_headers.Builder().add_header( HttpHeaderFields.ACCEPT.value, 'application/xml').add_header( HttpHeaderFields.CONTENT_TYPE.value, 'text/html').add_header(HttpHeaderFields.ACCEPT.value, 'image/webp').build() expected = ['application/xml', 'image/webp'] self.assertEqual(headers.get_all('accept'), expected) def test_get_all_with_known_header_value_canonicalizes_requested_header(self): headers = http_headers.Builder().add_header( HttpHeaderFields.ACCEPT.value, 'application/xml').add_header( HttpHeaderFields.CONTENT_TYPE.value, 'text/html').add_header(HttpHeaderFields.ACCEPT.value, 'image/webp').build() expected = ['application/xml', 'image/webp'] self.assertEqual(headers.get_all('ACCEPT'), expected) def test_get_all_when_request_value_does_not_exist_returns_empty_list(self): headers = http_headers.Builder().add_header( HttpHeaderFields.ACCEPT.value, 'application/xml').add_header( HttpHeaderFields.CONTENT_TYPE.value, 'text/html').add_header(HttpHeaderFields.ACCEPT.value, 'image/webp').build() self.assertEmpty(headers.get_all('cookie')) def test_get_all_with_none_header_name_raise_error(self): headers = http_headers.Builder().add_header(HttpHeaderFields.ACCEPT.value, 'app/xml').build() with self.assertRaises(ValueError) as exc: headers.get_all(None) self.assertEqual('Name cannot be None.', str(exc.exception)) if __name__ == '__main__': absltest.main() ================================================ FILE: plugin_server/py/common/net/http/http_method.py ================================================ """HTTP method for CRUD operations.""" import enum class HttpMethod(str, enum.Enum): """HTTP request type. Attributes: string: HTTP method. """ GET = "GET" HEAD = "HEAD" POST = "POST" PUT = "PUT" DELETE = "DELETE" def __init__(self, string): self.string = string ================================================ FILE: plugin_server/py/common/net/http/http_request.py ================================================ """HTTP request utility.""" from typing import Optional from common.net.http.http_headers import Builder as HttpHeadersBuilder from common.net.http.http_headers import HttpHeaders from common.net.http.http_method import HttpMethod def check_url_argument(func): def wrapper(cls, url): if not url: raise ValueError('Url cannot be None.') return func(cls, url) return wrapper class HttpRequest: """HTTP request utility class. Please use Builder() to create instances of this class. Attributes: method: The HTTP request type. url: String address of the request. headers: The HTTP request headers in key/value pairs. body: The HTTP body could be empty per the request type. GET and HEAD request types must have empty request_body. """ def __init__(self): self.method: HttpMethod = None self.url: Optional[str] = None self.headers: HttpHeaders = None self.body: Optional[bytes] = None @classmethod @check_url_argument def get(cls, url: str): return Builder().set_method(HttpMethod.GET).set_url(url) @classmethod @check_url_argument def head(cls, url: str): return Builder().set_method(HttpMethod.HEAD).set_url(url) @classmethod @check_url_argument def post(cls, url: str): return Builder().set_method(HttpMethod.POST).set_url(url) @classmethod @check_url_argument def put(cls, url: str): return Builder().set_method(HttpMethod.PUT).set_url(url) @classmethod @check_url_argument def delete(cls, url: str): return Builder().set_method(HttpMethod.DELETE).set_url(url) @classmethod def builder(cls): return Builder() class Builder: """Builder class to create HTTP request object. Attributes: http_request: HTTP request object built. """ def __init__(self): self.http_request = HttpRequest() def set_method(self, method: HttpMethod) -> 'Builder': """Set the HttpMethod type.""" self.http_request.method = method return self def set_url(self, url: str) -> 'Builder': """Set the string address for the request.""" self.http_request.url = url return self def set_headers(self, headers: HttpHeaders) -> 'Builder': """Set the HttpHeaders for the request.""" self.http_request.headers = headers return self def set_request_body(self, request_body: Optional[bytes] = None) -> 'Builder': """Set the request body.""" self.http_request.body = request_body return self def with_empty_headers(self) -> 'Builder': """Set an empty Http_headers for the request.""" self.set_headers(HttpHeadersBuilder().build()) return self def build(self) -> 'HttpRequest': if ( self.http_request.method == HttpMethod.GET or self.http_request.method == HttpMethod.HEAD ): if self.http_request.body: raise ValueError( 'A request body is not allowed for HTTP GET/HEAD request.') return self.http_request ================================================ FILE: plugin_server/py/common/net/http/http_request_test.py ================================================ """Tests for google3.third_party.java_src.tsunami.plugin_server.py.common.net.http_request.""" from absl.testing import absltest from absl.testing import parameterized from common.net.http.http_headers import HttpHeaders from common.net.http.http_method import HttpMethod from common.net.http.http_request import HttpRequest class HttpRequestTest(parameterized.TestCase): @parameterized.named_parameters( ('get_method', HttpRequest.get), ('head_method', HttpRequest.head), ('post_method', HttpRequest.post), ('delete_method', HttpRequest.delete)) def test_http_request_methods_with_empty_url_raise_error( self, request_method): with self.assertRaises(ValueError) as exc: request_method('') self.assertEqual(str(exc.exception), 'Url cannot be None.') def test_get_builds_http_get_request(self): request = HttpRequest.get( 'http://localhost/url').with_empty_headers().build() self.assertEqual(request.method, HttpMethod.GET) self.assertEqual(request.url, 'http://localhost/url') def test_head_builds_http_head_request(self): request = HttpRequest.head( 'http://localhost/url').with_empty_headers().build() self.assertEqual(request.method, HttpMethod.HEAD) self.assertEqual(request.url, 'http://localhost/url') def test_head_builds_http_post_request(self): request = HttpRequest.post( 'http://localhost/url').with_empty_headers().build() self.assertEqual(request.method, HttpMethod.POST) self.assertEqual(request.url, 'http://localhost/url') def test_head_builds_http_delete_request(self): request = HttpRequest.delete( 'http://localhost/url').with_empty_headers().build() self.assertEqual(request.method, HttpMethod.DELETE) self.assertEqual(request.url, 'http://localhost/url') def test_put_builds_http_put_request(self): request = HttpRequest.put( 'http://localhost/url').with_empty_headers().build() self.assertEqual(request.method, HttpMethod.PUT) self.assertEqual(request.url, 'http://localhost/url') def test_get_with_request_body_raise_error(self): with self.assertRaises(ValueError) as exc: HttpRequest.builder().set_method( HttpMethod.GET).set_url('http://localhost/url').set_headers( HttpHeaders.builder()).set_request_body(bytes( 'abc', 'utf-8')).build() self.assertEqual('A request body is not allowed for HTTP GET/HEAD request.', str(exc.exception)) if __name__ == '__main__': absltest.main() ================================================ FILE: plugin_server/py/common/net/http/http_response.py ================================================ """HTTP response utility.""" import abc from collections.abc import Mapping import json from typing import Any from common.net.http.http_headers import HttpHeaders from common.net.http.http_status import HttpStatus class HttpResponse(metaclass=abc.ABCMeta): """HTTP request utility class. Please use Builder() to create instances of this class. Attributes: status: The HTTP response status. url: String address that produced this HTTP response. headers: The HTTP response headers in key/value pairs. body: The HTTP body data. """ def __init__(self): self.status: HttpStatus | None = None self.url: str | None = None self.headers: HttpHeaders | None = None self.body: bytes | None = None def body_string(self) -> str: """Get the body data as a UTF-8 encoded string.""" if self.body is None: return '' return self.body.decode('utf-8') def body_json(self) -> Mapping[str, Any] | None: """Parse the response body as JSON. Returns null if parsing failed.""" try: return json.loads(self.body_string()) except ValueError: return None def json_field_has_value(self, field: str, value: str): """Check if JSON body of the response has field and value pair.""" body = self.body_json() if body is not None: return body.get(field) == value return False @classmethod def builder(cls): return Builder() class Builder: """Builder class to create HTTP request object. Attributes: http_response: HTTP request object built. """ def __init__(self): self.http_response = HttpResponse() def set_status(self, status: HttpStatus): """Set the HttpStatus type.""" self.http_response.status = status return self def set_headers(self, headers: HttpHeaders): """Set the HttpHeaders for the response.""" self.http_response.headers = headers return self def set_response_body(self, response_body: bytes | None): """Set the body for the response.""" self.http_response.body = response_body return self def set_url(self, url: str): """Set URL response.""" self.http_response.url = url return self def build(self): return self.http_response ================================================ FILE: plugin_server/py/common/net/http/http_response_test.py ================================================ """Tests for google3.third_party.java_src.tsunami.plugin_server.py.common.net.http_response.""" from absl.testing import absltest from common.net.http.http_headers import HttpHeaders from common.net.http.http_response import HttpResponse from common.net.http.http_status import HttpStatus class HttpResponseTest(absltest.TestCase): def test_body_json_with_valid_response_body_returns_parsed_json(self): response = HttpResponse.builder().set_status( HttpStatus.OK).set_response_body( bytes('{ \"test_value\": 1 }', 'utf-8')).set_url('http://localhost/url').build() self.assertNotEmpty(response.body_json()) self.assertTrue(type(response.body_json()), 'json') self.assertEqual(response.body_json()['test_value'], 1) def test_body_json_with_no_response_body_returns_null(self): response = HttpResponse.builder().set_status(HttpStatus.OK).set_headers( HttpHeaders.builder().build()).set_url('http://localhost/url').build() self.assertIsNone(response.body_json()) def test_body_json_with_non_json_response_body_returns_null(self): response = HttpResponse.builder().set_status(HttpStatus.OK).set_headers( HttpHeaders.builder().build()).set_response_body(bytes( 'abc', 'utf-8')).set_url('http://localhost/url').build() self.assertIsNone(response.body_json()) def test_json_field_has_value_with_empty_json_body_returns_false(self): response = HttpResponse.builder().set_status(HttpStatus.OK).set_headers( HttpHeaders.builder().build()).set_response_body(bytes( '{ }', 'utf-8')).set_url('http://localhost/url').build() self.assertFalse(response.json_field_has_value('field', 'value')) def test_json_field_has_value_with_no_body_returns_false(self): response = HttpResponse.builder().set_status(HttpStatus.OK).set_headers( HttpHeaders.builder().build()).set_url('http://localhost/url').build() self.assertFalse(response.json_field_has_value('field', 'value')) def test_json_field_has_value_with_non_empty_json_body_returns_true(self): response = HttpResponse.builder().set_status(HttpStatus.OK).set_headers( HttpHeaders.builder().build()).set_response_body( bytes('{ \"field\": \"value\"}', 'utf-8')).set_url('http://localhost/url').build() self.assertTrue(response.json_field_has_value('field', 'value')) def test_is_success_with_unspecified_status_returns_false(self): response = HttpResponse.builder().set_status( HttpStatus.HTTP_STATUS_UNSPECIFIED).set_headers( HttpHeaders.builder().build()).set_url( 'http://localhost/url').build() self.assertFalse(response.status.is_success()) if __name__ == '__main__': absltest.main() ================================================ FILE: plugin_server/py/common/net/http/http_status.py ================================================ """Http Status Codes utility class.""" import aenum as enum class HttpStatus(enum.MultiValueEnum): """HTTP Status Codes defined in RFC 2616, RFC 6585, RFC 4918 and RFC 7538. @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html @see http://tools.ietf.org/html/rfc6585 @see https://tools.ietf.org/html/rfc4918 @see https://tools.ietf.org/html/rfc7538 Attributes: from_code: Get the HttpStatus from status code. is_redirect: Check if it is a redirected status response. is_success: Check if it is a success status response. to_string: Get the status name. """ # Default HTTP_STATUS_UNSPECIFIED = 0, "Status Unspecified" # Informational 1xx CONTINUE = 100, "Continue" SWITCHING_PROTOCOLS = 101, "Switching Protocols" # Successful 2xx OK = 200, "OK" CREATED = 201, "Created" ACCEPTED = 202, "Accepted" NON_AUTHORITATIVE_INFORMATION = 203, "Non-Authoritative Information" NO_CONTENT = 204, "No Content" RESET_CONTENT = 205, "Reset Content" PARTIAL_CONTENT = 206, "Partial Content" MULTI_STATUS = 207, "Multi-Status" # Redirection 3xx MULTIPLE_CHOICES = 300, "Multiple Choices" MOVED_PERMANENTLY = 301, "Moved Permanently" FOUND = 302, "Found" SEE_OTHER = 303, "See Other" NOT_MODIFIED = 304, "Not Modified" USE_PROXY = 305, "Use Proxy" TEMPORARY_REDIRECT = 307, "Temporary Redirect" PERMANENT_REDIRECT = 308, "Permanent Redirect" # Client Error 4xx BAD_REQUEST = 400, "Bad Request" UNAUTHORIZED = 401, "Unauthorized" PAYMENT_REQUIRED = 402, "Payment Required" FORBIDDEN = 403, "Forbidden" NOT_FOUND = 404, "Not Found" METHOD_NOT_ALLOWED = 405, "Method Not Allowed" NOT_ACCEPTABLE = 406, "Not Acceptable" PROXY_AUTHENTICATION_REQUIRED = 407, "Proxy Authentication Required" REQUEST_TIMEOUT = 408, "Request Timeout" CONFLICT = 409, "Conflict" GONE = 410, "Gone" LENGTH_REQUIRED = 411, "Length Required" PRECONDITION_FAILED = 412, "Precondition Failed" REQUEST_ENTITY_TOO_LARGE = 413, "Request Entity Too Large" REQUEST_URI_TOO_LONG = 414, "Request URI Too Long" UNSUPPORTED_MEDIA_TYPE = 415, "Unsupported Media Type" REQUEST_RANGE_NOT_SATISFIABLE = 416, "Request Range Not Satisfiable" EXPECTATION_FAILED = 417, "Expectation Failed" UNPROCESSABLE_ENTITY = 422, "Unprocessable Entity" LOCKED = 423, "Locked" FAILED_DEPENDENCY = 424, "Failed Dependency" PRECONDITION_REQUIRED = 428, "Precondition Required" TOO_MANY_REQUESTS = 429, "Too Many Requests" REQUEST_HEADER_FIELDS_TOO_LARGE = 431, "Request Header Fields Too Large" # Server Error 5xx INTERNAL_SERVER_ERROR = 500, "Internal Server Error" NOT_IMPLEMENTED = 501, "Not Implemented" BAD_GATEWAY = 502, "Bad Gateway" SERVICE_UNAVAILABLE = 503, "Service Unavailable" GATEWAY_TIMEOUT = 504, "Gateway Timeout" HTTP_VERSION_NOT_SUPPORTED = 505, "HTTP Version Not Supported" INSUFFICIENT_STORAGE = 507, "Insufficient Storage" NETWORK_AUTHENTICATION_REQUIRED = 511, "Network Authentication Required" # IE returns 1223 for 'Operation Aborted' instead of 204 with the status text # 'Unknown' and an empty response headers. Known to occur on IE 6 on XP # through IE9 on Window 7. QUIRK_IE_NO_CONTENT = 1223, "Quirk IE No Content" @classmethod def from_code(cls, code: int): """Gets the HTTP status from the status code. Args: code: The HTTP status code. Returns: The matching HTTP status or HTTP_STATUS_UNSPECIFIED if no known status is found. """ try: status = HttpStatus(code) return status except ValueError: return HttpStatus.HTTP_STATUS_UNSPECIFIED def __init__(self, code, message=""): self.code = code self.message = message def __str__(self) -> str: return self.message def is_redirect(self) -> bool: return bool(self in _REDIRECTED_HTTP_STATUS) def is_success(self) -> bool: return self.code >= 200 and self.code < 300 _REDIRECTED_HTTP_STATUS = [ HttpStatus.MULTIPLE_CHOICES, HttpStatus.MOVED_PERMANENTLY, HttpStatus.FOUND, HttpStatus.SEE_OTHER, HttpStatus.TEMPORARY_REDIRECT, HttpStatus.PERMANENT_REDIRECT, ] ================================================ FILE: plugin_server/py/common/net/http/http_status_test.py ================================================ """Tests for google3.third_party.java_src.tsunami.plugin_server.py.common.net.http_status.""" from absl.testing import absltest from absl.testing import parameterized from common.net.http import http_status HttpStatus = http_status.HttpStatus class HttpStatusTest(parameterized.TestCase): def test_from_code_with_status_code_returns_http_status(self): status = HttpStatus.from_code(200) self.assertEqual(status, HttpStatus.OK) def test_from_code_with_invalid_status_code_returns_http_status_unspecified( self): status = HttpStatus.from_code(1) self.assertEqual(status, HttpStatus.HTTP_STATUS_UNSPECIFIED) @parameterized.named_parameters( ('with_multiple_choices', HttpStatus.MULTIPLE_CHOICES), ('with_moved_permanently', HttpStatus.MOVED_PERMANENTLY), ('with_found', HttpStatus.FOUND), ('with_see_other', HttpStatus.SEE_OTHER), ('with_temporary_redirect', HttpStatus.TEMPORARY_REDIRECT), ('with_permanent_redirect', HttpStatus.PERMANENT_REDIRECT)) def test_is_redirect_returns_true(self, status): self.assertTrue(status.is_redirect()) def test_is_redirect_with_non_redirected_status_returns_false(self): self.assertFalse(HttpStatus.LOCKED.is_redirect()) def test_is_success_with_code_between_199_and_300(self): self.assertTrue(HttpStatus.from_code(200).is_success()) self.assertTrue(HttpStatus.from_code(207).is_success()) self.assertFalse(HttpStatus.from_code(300).is_success()) def test___str__returns_status_message(self): self.assertEqual(HttpStatus.CONTINUE.__str__(), 'Continue') if __name__ == '__main__': absltest.main() ================================================ FILE: plugin_server/py/common/net/http/requests_http_client.py ================================================ """HTTP client using requests.""" import asyncio import concurrent.futures import functools from typing import Optional from absl import logging import requests from common.data.network_service_utils import NetworkService from common.net.http.host_resolver_http_adapter import HostResolverHttpAdapter from common.net.http.http_client import Builder from common.net.http.http_client import HttpClient from common.net.http.http_header_fields import HttpHeaderFields from common.net.http.http_headers import HttpHeaders from common.net.http.http_request import HttpRequest from common.net.http.http_response import HttpResponse from common.net.http.http_status import HttpStatus _DEFAULT_ALLOW_REDIRECT = True _DEFAULT_POOL_CONNECTIONS = 5 _DEFAULT_POOL_MAXSIZE = 10 _DEFAULT_MAX_WORKERS = 64 _TIMEOUT_SEC = 10 _VERIFY_SSL = True class RequestsHttpClient(HttpClient): """Requests HTTP client library used for communicating with remote servers. Attributes: allow_redirects: Optional boolean to determine whether requests may be redirected. True by default. log_id: the log id. max_workers: Maximum number of threads to execute concurrently. session: Requests session object to manage and persist settings across requests. timeout_sec: Optional float. How long to wait for the server to send data before giving up. verify_ssl: Optional boolean to verify SSL certification. True by default. """ TSUNAMI_USER_AGENT = 'TsunamiSecurityScanner' def __init__( self, session: requests.Session, allow_redirects: Optional[bool], log_id: Optional[str], max_workers: Optional[int], timeout_sec: Optional[float], verify_ssl: Optional[bool], ): self.session = session self.allow_redirects = allow_redirects self.log_id = log_id self.max_workers = max_workers self.timeout_sec = timeout_sec self.verify_ssl = verify_ssl def get_log_id(self) -> str: return self.log_id def send(self, http_request: HttpRequest, network_service: Optional[NetworkService] = None) -> HttpResponse: """Send the HTTP request using this client.""" logging.info("%sSending HTTP '%s' request to '%s'.", self.log_id, http_request.method, http_request.url) req = self._prepare_request(http_request) resp = self.session.send( request=req, ip=self._get_ip(network_service), verify=self.verify_ssl, timeout=self.timeout_sec, allow_redirects=self.allow_redirects, ) return self._parse_response(resp) def send_async( self, http_request: HttpRequest, network_service: Optional[NetworkService] = None) -> HttpResponse: """Send the HTTP request asynchronously.""" logging.info("%sSending HTTP '%s' request to '%s'.", self.log_id, http_request.method, http_request.url) req = self._prepare_request(http_request) loop = asyncio.get_event_loop() future = asyncio.ensure_future(self._prepare_future(req, network_service)) loop.run_until_complete(future) res = future.result() return self._parse_response(res) @classmethod def modify(cls): """Allows client code to modify the configurations of the HTTP client.""" return RequestsHttpClientBuilder() def _build_response_headers(self, headers: dict[str, str]) -> HttpHeaders: headers_builder = HttpHeaders.builder() for field in headers: headers_builder.add_header(field, headers[field]) return headers_builder.build() def _parse_response(self, res: requests.Response) -> HttpResponse: response_header = self._build_response_headers(res.headers) status = HttpStatus.from_code(res.status_code) return ( HttpResponse.builder() .set_url(res.url) .set_status(status) .set_headers(response_header) .set_response_body(res.content) .build() ) async def _prepare_future( self, req: requests.PreparedRequest, network_service: Optional[NetworkService], ): """Prepare async request to include configuration.""" loop = asyncio.get_event_loop() future = loop.run_in_executor( concurrent.futures.ThreadPoolExecutor(max_workers=self.max_workers), functools.partial( self.session.send, request=req, ip=self._get_ip(network_service), verify=self.verify_ssl, timeout=self.timeout_sec, allow_redirects=self.allow_redirects, ), ) return await future def _prepare_request( self, http_request: HttpRequest ) -> requests.PreparedRequest: """Prepare request to bypass Requests library's canonicalization. Client can accept requests with any URL paths such as paths with "../.." and special characters. Args: http_request: HTTP request to prep for. Returns: PreparedRequest: Request containing the exact bytes that is ready to be sent. """ req = requests.Request( method=http_request.method, url=http_request.url, data=http_request.body, headers=self._serialize_request_headers(http_request.headers) ) prepped = req.prepare() # URL reassignment to bypass URL canonicalization prepped.url = http_request.url return prepped def _serialize_request_headers(self, headers: HttpHeaders) -> dict[str, str]: """Put headers in a dictionary and add Tsunami user agent.""" serialized_headers = {} for field, values in headers.raw_headers.items(): serialized_headers[field] = ', '.join(values) serialized_headers[ HttpHeaderFields.USER_AGENT.value] = self.TSUNAMI_USER_AGENT return serialized_headers def _get_ip(self, network_service: Optional[NetworkService]) -> Optional[str]: if not network_service: return None return network_service.network_endpoint.ip_address.address class RequestsHttpClientBuilder(Builder): """Base builder for implementations of RequestsHttpClient.""" def __init__(self): self.log_id = None # SSL certification verification. self.verify_ssl = _VERIFY_SSL # How long to wait for the server to send data before giving up. self.timeout_sec = _TIMEOUT_SEC # Whether requests may be redirected. self.allow_redirects = _DEFAULT_ALLOW_REDIRECT # Maximum number of threads to execute concurrently. self.max_workers = _DEFAULT_MAX_WORKERS # Number of urllib3 connection pools to cache. self.pool_connections = _DEFAULT_POOL_CONNECTIONS # Maximum number of connections to save in the pool. self.pool_maxsize = _DEFAULT_POOL_MAXSIZE def set_allow_redirects( self, allow_redirects: bool ) -> 'RequestsHttpClientBuilder': self.allow_redirects = allow_redirects return self def set_log_id(self, log_id: str) -> 'RequestsHttpClientBuilder': self.log_id = log_id return self def set_max_workers(self, max_workers: int) -> 'RequestsHttpClientBuilder': self.max_workers = max_workers return self def set_pool_connections( self, pool_connections: int ) -> 'RequestsHttpClientBuilder': self.pool_connections = pool_connections return self def set_pool_maxsize(self, pool_maxsize: int) -> 'RequestsHttpClientBuilder': self.pool_maxsize = pool_maxsize return self def set_timeout_sec(self, timeout_sec: float) -> 'RequestsHttpClientBuilder': self.timeout_sec = timeout_sec return self def set_verify_ssl(self, verify_ssl: bool) -> 'RequestsHttpClientBuilder': self.verify_ssl = verify_ssl return self def build(self) -> RequestsHttpClient: session = requests.Session() adapter = HostResolverHttpAdapter( pool_maxsize=self.pool_maxsize, pool_connections=self.pool_connections, ) session.mount('http://', adapter) session.mount('https://', adapter) return RequestsHttpClient( session=session, allow_redirects=self.allow_redirects, log_id=self.log_id, max_workers=self.max_workers, timeout_sec=self.timeout_sec, verify_ssl=self.verify_ssl, ) ================================================ FILE: plugin_server/py/common/net/http/requests_http_client_test.py ================================================ """Tests for google3.third_party.java_src.tsunami.plugin_server.py.common.net.requests_http_client.""" import unittest from absl.testing import absltest import requests import requests_mock from common.data import network_endpoint_utils from common.net.http.host_resolver_http_adapter import HostResolverHttpAdapter from common.net.http.http_header_fields import HttpHeaderFields from common.net.http.http_headers import HttpHeaders from common.net.http.http_method import HttpMethod from common.net.http.http_request import HttpRequest from common.net.http.http_response import HttpResponse from common.net.http.http_status import HttpStatus from common.net.http.requests_http_client import RequestsHttpClient from common.net.http.requests_http_client import RequestsHttpClientBuilder import network_pb2 import network_service_pb2 class RequestsHttpClientTest(absltest.TestCase): @classmethod def setUpClass(cls): super().setUpClass() cls.client = RequestsHttpClientBuilder().build() def test_get_log_id(self): self.assertEqual(self.client.modify().set_log_id(1).build().get_log_id(), 1) @requests_mock.mock() def test_send_returns_expected_http_response(self, mock): url = 'http://example.com/send/%2e%2e/%2e%2e/etc/path' mock.register_uri(HttpMethod.GET, url) response = self.client.send( HttpRequest().get(url).with_empty_headers().build() ) expected = self._create_expected_response( url, headers=HttpHeaders().builder().build() ) self._assert_response_is_expected(response, expected) @requests_mock.mock() def test_send_with_get_request_returns_expected_http_response(self, mock): body = 'GET BODY'.encode('utf-8') url = 'http://example.com/get/[]%$/test-path' header_field = HttpHeaderFields.CONTENT_TYPE.value header_value = 'text/html; charset=utf-8' mock.register_uri( HttpMethod.GET, url, content=body, headers={header_field: header_value} ) response = self.client.send( HttpRequest() .get(url) .with_empty_headers() .build() ) expected = self._create_expected_response(url, body=body) self._assert_response_is_expected(response, expected) @requests_mock.mock() def test_send_async_with_get_request_returns_expected_http_response( self, mock ): body = 'GET BODY'.encode('utf-8') url = 'http://example.com/get/[]%$/test-path' header_field = HttpHeaderFields.CONTENT_TYPE.value header_value = 'text/html; charset=utf-8' mock.register_uri( HttpMethod.GET, url, content=body, headers={header_field: header_value} ) response = self.client.send_async( HttpRequest() .get(url) .with_empty_headers() .build() ) expected = self._create_expected_response(url, body=body) self._assert_response_is_expected(response, expected) @requests_mock.mock() def test_send_with_head_request_returns_http_response_without_body( self, mock ): body = 'Body should not exist.'.encode('utf-8') url = 'http://example.com/send/[]%$/test-path' header_field = HttpHeaderFields.CONTENT_TYPE.value header_value = 'text/html; charset=utf-8' mock.register_uri( HttpMethod.HEAD, url, content=body, headers={header_field: header_value} ) response = self.client.send( HttpRequest() .head(url) .with_empty_headers() .build() ) expected = self._create_expected_response(url, body=body) self._assert_response_is_expected(response, expected) @requests_mock.mock() def test_send_async_with_head_request_returns_expected_http_response_without_body( self, mock ): body = 'HEAD BODY'.encode('utf-8') url = 'http://example.com/post/[]%$/test-path' header_field = HttpHeaderFields.CONTENT_TYPE.value header_value = 'text/html; charset=utf-8' mock.register_uri( HttpMethod.HEAD, url, content=body, headers={header_field: header_value} ) response = self.client.send_async( HttpRequest() .head(url) .with_empty_headers() .build() ) expected = self._create_expected_response(url, body=body) self._assert_response_is_expected(response, expected) @requests_mock.mock() def test_send_with_post_request_returns_expected_http_response( self, mock ): body = 'POST BODY'.encode('utf-8') url = 'http://example.com/post/[]%$/test-path' header_field = HttpHeaderFields.CONTENT_TYPE.value header_value = 'text/html; charset=utf-8' mock.register_uri( HttpMethod.POST, url, content=body, headers={header_field: header_value} ) response = self.client.send( HttpRequest() .post(url) .set_headers( HttpHeaders() .builder() .add_header(header_field, header_value) .build() ) .build() ) expected = self._create_expected_response(url, body=body) self._assert_response_is_expected(response, expected) @requests_mock.mock() def test_send_async_with_post_request_returns_expected_http_response( self, mock ): body = 'POST BODY'.encode('utf-8') url = 'http://example.com/post/[]%$/test-path' header_field = HttpHeaderFields.CONTENT_TYPE.value header_value = 'text/html; charset=utf-8' mock.register_uri( HttpMethod.POST, url, content=body, headers={header_field: header_value} ) response = self.client.send_async( HttpRequest() .post(url) .set_headers( HttpHeaders() .builder() .add_header(header_field, header_value) .build() ) .build() ) expected = self._create_expected_response(url, body=body) self._assert_response_is_expected(response, expected) @requests_mock.mock() def test_send_with_post_request_with_empty_headers_returns_expected_http_response( self, mock ): body = '{ "test": "json" }'.encode('utf-8') url = 'http://example.com/post/test-path' header_field = HttpHeaderFields.CONTENT_TYPE.value header_value = 'text/html; charset=utf-8' mock.register_uri( HttpMethod.POST, url, content=body, headers={header_field: header_value} ) response = self.client.send( HttpRequest() .post(url) .with_empty_headers() .build() ) expected = self._create_expected_response(url, body=body) self._assert_response_is_expected(response, expected) @requests_mock.mock() def test_send_async_with_post_request_with_empty_headers_returns_expected_http_response( self, mock ): body = '{ \"test\": \"json\" }'.encode('utf-8') url = 'http://example.com/post/test-path' header_field = HttpHeaderFields.CONTENT_TYPE.value header_value = 'text/html; charset=utf-8' mock.register_uri( HttpMethod.POST, url, content=body, headers={header_field: header_value} ) response = self.client.send_async( HttpRequest().post(url).with_empty_headers().build() ) expected = self._create_expected_response( url, body='{ "test": "json" }'.encode('utf-8') ) self._assert_response_is_expected(response, expected) @requests_mock.mock() def test_send_with_hostname_and_ip_use_hostname_as_proxy(self, mock): url = 'http://example.com/post/test-path' network_endpoint = network_endpoint_utils.for_ip_and_hostname( '127.0.0.1', 'proxy.com' ) network_service = network_service_pb2.NetworkService( network_endpoint=network_endpoint, transport_protocol=network_pb2.TransportProtocol.TCP, ) adapter = HostResolverHttpAdapter(5, 10) requests.Session.get_adapter = unittest.mock.MagicMock(return_value=adapter) mock.register_uri(HttpMethod.GET, url) response = self.client.send( HttpRequest() .get(url) .with_empty_headers() .build(), network_service=network_service, ) expected = self._create_expected_response( url, headers=HttpHeaders.builder().build() ) self._assert_response_is_expected(response, expected) @requests_mock.mock() def test_send_async_with_hostname_and_ip_use_hostname_as_proxy(self, mock): url = 'http://example.com/post/test-path' network_endpoint = network_endpoint_utils.for_ip_and_hostname( '127.0.0.1', 'proxy.com' ) network_service = network_service_pb2.NetworkService( network_endpoint=network_endpoint, transport_protocol=network_pb2.TransportProtocol.TCP, ) adapter = HostResolverHttpAdapter(5, 10) requests.Session.get_adapter = unittest.mock.MagicMock(return_value=adapter) mock.register_uri(HttpMethod.GET, url) response = self.client.send_async( HttpRequest() .get(url) .with_empty_headers() .build(), network_service=network_service, ) expected = self._create_expected_response( url, headers=HttpHeaders.builder().build() ) self._assert_response_is_expected(response, expected) def test_requests_http_client_default_configs(self): self.assertTrue(self.client.allow_redirects) self.assertTrue(self.client.verify_ssl) @requests_mock.mock() def test_send_with_modified_configuration(self, mock): url = 'http://example.com/post/test-path' network_endpoint = network_endpoint_utils.for_ip_and_hostname( '127.0.0.1', 'proxy.com' ) network_service = network_service_pb2.NetworkService( network_endpoint=network_endpoint, transport_protocol=network_pb2.TransportProtocol.TCP, ) mock.register_uri(HttpMethod.GET, url) client = ( RequestsHttpClient.modify() .set_timeout_sec(1.1) .set_verify_ssl(False) .set_allow_redirects(False) .set_max_workers(2) .set_pool_connections(1) .set_pool_maxsize(1) ) self.assertFalse(client.allow_redirects) self.assertFalse(client.verify_ssl) self.assertEqual(client.timeout_sec, 1.1) self.assertEqual(client.max_workers, 2) response = client.build().send( HttpRequest().get(url).with_empty_headers().build(), network_service=network_service, ) expected = self._create_expected_response( url, headers=HttpHeaders.builder().build() ) self._assert_response_is_expected(response, expected) def test_send_when_request_failed_raise_error(self): url = 'http://example.com/post/test-path' with self.assertRaises(requests.exceptions.RequestException): self.client.send( HttpRequest().post(url).with_empty_headers().build() ) def test_send_async_when_request_failed_raise_error(self): url = 'http://example.com/delete/test-path' with self.assertRaises(requests.exceptions.RequestException): self.client.send_async( HttpRequest().delete(url).with_empty_headers().build() ) def test__serialize_request_headers_include_custom_user_agent(self): field = HttpHeaderFields.CONTENT_TYPE.value value = 'text/html; charset=utf-8' headers = HttpHeaders.builder().add_header(field, value).build() self.assertEqual( self.client._serialize_request_headers(headers), { field: value, HttpHeaderFields.USER_AGENT.value: 'TsunamiSecurityScanner', }, ) def _assert_response_is_expected(self, response, expected): self.assertEqual(response.body_json(), expected.body_json()) self.assertEqual(response.status, expected.status) self.assertEqual(response.url, expected.url) self.assertEqual(response.headers.raw_headers, expected.headers.raw_headers) def _create_expected_response( self, url, status=HttpStatus.OK, headers=HttpHeaders.builder() .add_header( HttpHeaderFields.CONTENT_TYPE.value, 'text/html; charset=utf-8' ) .build(), body=None, ): return ( HttpResponse() .builder() .set_url(url) .set_status(status) .set_headers(headers) .set_response_body(body) .build() ) if __name__ == '__main__': absltest.main() ================================================ FILE: plugin_server/py/plugin/payload/payload.py ================================================ """Payload is the type returned by the Payload Generator.""" from typing import Optional from absl import logging from plugin.payload.validator import Validator import payload_generator_pb2 class Payload: """Out-of-bound payload to be sent to the scan target.""" def __init__( self, payload: str, validator: Validator, attributes: payload_generator_pb2.PayloadAttributes, config: payload_generator_pb2.PayloadGeneratorConfig, ): self.payload = payload self.validator = validator self.attributes = attributes self.config = config def get_payload(self) -> str: """Get the string representation of the payload. Returns: The payload string """ logging.info( '%s generated payload `%s`, %s use the callback server', self.config, self.payload, 'does' if self.attributes.uses_callback_server else 'does not', ) return self.payload def check_if_executed( self, payload_data: Optional[str | bytes] = None ) -> bool: """Check if the payload was executed on the scan target. Args: payload_data: Optional string or bytees representation of the payload to be verified. Returns: Whether the payload was executed. """ payload = ( payload_data.encode('utf-8') if isinstance(payload_data, str) else payload_data ) return self.validator.is_executed(payload) def get_payload_attributes(self) -> payload_generator_pb2.PayloadAttributes: """Get the payload attributes. Returns: Details of the payload. """ return self.attributes ================================================ FILE: plugin_server/py/plugin/payload/payload_generator.py ================================================ """Payload generator create custom payload for Tsunami detectors.""" import re from typing import Any, Callable, Optional from plugin.payload.payload import Payload from plugin.payload.payload_secret_generator import PayloadSecretGenerator from plugin.payload.validator import Validator from plugin.tcs_client import TcsClient import payload_generator_pb2 as pg class PayloadGenerator: """Select a payload with the given payload generator config.""" SECRET_LENGTH = 8 TOKEN_CALLBACK_SERVER_URL = '$TSUNAMI_PAYLOAD_TOKEN_URL' TOKEN_RANDOM_STRING = '$TSUNAMI_PAYLOAD_TOKEN_RANDOM' def __init__( self, payload_secret_generator: PayloadSecretGenerator, payloads: list[pg.PayloadDefinition], tcs_client: TcsClient, ): """Initialize Payload Generator. Args: payload_secret_generator: A secret generator used to create a one-time string for payload use. payloads: A list of pre-generated payload definitions for selection. tcs_client: UI for interacting with the Tsunami Callback Server. """ self.payload_secret_generator = payload_secret_generator self.payloads = payloads self.tcs_client = tcs_client def is_callback_server_enabled(self): """Check if callback server is enabled. Returns: Whether the callback server is enabled. """ return self.tcs_client.is_callback_server_enabled() def generate(self, config: pg.PayloadGeneratorConfig) -> Payload: """Find a matching payload that uses callback server. The algorithm prioritizes the matching payload with callback server enabled and falls back to any other matching payload. Args: config: Payload generator config detailing the attributes required for the selected payload. Returns: A matching payload per the payload generator config. """ return self._generate_payload(config, True) def generate_no_callback(self, config: pg.PayloadGeneratorConfig) -> Payload: """Find a matching payload that does not uses callback server. Args: config: Payload generator config detailing the attributes required for the selected payload. Returns: A matching payload per the payload generator config. """ return self._generate_payload(config, False) def _generate_payload( self, config: pg.PayloadGeneratorConfig, use_callback: bool ) -> Payload: """Find matching payload per the provided attributes.""" payload = None if self.tcs_client.is_callback_server_enabled() and use_callback: payload = self._find_matching_payload(config, use_callback) if not payload: payload = self._find_matching_payload(config, False) if not payload: raise LookupError( 'No payload implemented for %s vulnerability type, %s interpretation' ' environment, and %s execution environment.' % ( pg.PayloadGeneratorConfig.VulnerabilityType.Name( config.vulnerability_type ), pg.PayloadGeneratorConfig.InterpretationEnvironment.Name( config.interpretation_environment ), pg.PayloadGeneratorConfig.ExecutionEnvironment.Name( config.execution_environment ), ) ) return payload def _find_matching_payload( self, config: pg.PayloadGeneratorConfig, use_callback: bool ) -> Optional[Payload]: for payload in self.payloads: if (self._payload_matches_config(payload, config, use_callback)): return self._parse_payload(payload, config) def _parse_payload( self, payload: pg.PayloadDefinition, config: pg.PayloadGeneratorConfig ) -> Payload: """Create payload from the selected payload definition.""" secret = self.payload_secret_generator.generate(self.SECRET_LENGTH) if bool(payload.uses_callback_server.ByteSize()): payload_string = payload.payload_string.value.replace( self.TOKEN_CALLBACK_SERVER_URL, self.tcs_client.get_callback_uri(secret), ) validator = type( 'PayloadValidator', (Validator,), {'is_executed': lambda s, _: self.tcs_client.has_oob_log(secret)}, )() return Payload( payload_string, validator, pg.PayloadAttributes(uses_callback_server=True), config, ) else: payload_string = payload.payload_string.value.replace( self.TOKEN_RANDOM_STRING, secret ) if payload.validation_type != pg.PayloadValidationType.Value( 'VALIDATION_REGEX' ): raise NotImplementedError( 'Validation type %s not supported.' % pg.PayloadGeneratorConfig.VulnerabilityType.Name( config.vulnerability_type) ) regex = payload.validation_regex.value.replace( self.TOKEN_RANDOM_STRING, secret ) validator = type( 'PayloadValidator', (Validator,), {'is_executed': _is_executed(regex)}, )() return Payload( payload_string, validator, pg.PayloadAttributes(uses_callback_server=False), config, ) def _payload_matches_config( self, payload: pg.PayloadDefinition, config: pg.PayloadGeneratorConfig, use_callback: bool, ) -> bool: return ( config.vulnerability_type in payload.vulnerability_type and config.interpretation_environment == payload.interpretation_environment and config.execution_environment == payload.execution_environment and bool(payload.uses_callback_server.ByteSize()) == use_callback ) def _is_executed(regex: str) -> Callable[[Any, Optional[bytes]], bool]: """Check if the returned payload is executed by validating against the regex.""" def check_payload_execution(_, data: Optional[bytes]) -> bool: if data is None: raise ValueError('No valid payload input is entered.') string = data.decode('utf-8') return bool(re.compile(regex).search(string)) or False return check_payload_execution ================================================ FILE: plugin_server/py/plugin/payload/payload_generator_test.py ================================================ """Tests for google3.third_party.java_src.tsunami.plugin_server.py.plugin.payload.payload_generator.""" import hashlib import unittest from absl.testing import absltest from absl.testing import parameterized import requests_mock from google.protobuf import wrappers_pb2 from common.net.http.requests_http_client import RequestsHttpClientBuilder from plugin.payload.payload_generator import PayloadGenerator from plugin.payload.payload_generator_test_helper import ANY_SSRF_CONFIG from plugin.payload.payload_generator_test_helper import JAVA_REFLECTIVE_RCE_CONFIG from plugin.payload.payload_generator_test_helper import LINUX_REFLECTIVE_RCE_CONFIG from plugin.payload.payload_generator_test_helper import LINUX_UNSPECIFIED_CONFIG from plugin.payload.payload_secret_generator import PayloadSecretGenerator from plugin.payload.payload_utility import get_parsed_payload from plugin.tcs_client import TcsClient import payload_generator_pb2 as pg _IP_ADDRESS = '127.0.0.1' _PORT = 8000 _URL = 'http://valid.com' _SECRET = '_1234_HERE_IT_IS_' class PayloadGeneratorWithCallbackTest(parameterized.TestCase): @classmethod def setUpClass(cls): super().setUpClass() psg = PayloadSecretGenerator() psg.generate = unittest.mock.MagicMock(return_value=_SECRET) client = TcsClient( _IP_ADDRESS, _PORT, _URL, RequestsHttpClientBuilder().build() ) payloads = get_parsed_payload() cls.payload_generator = PayloadGenerator(psg, payloads, client) def test_is_callback_server_enabled_returns_true(self): self.assertTrue(self.payload_generator.is_callback_server_enabled()) @parameterized.named_parameters( ('linux_config', LINUX_REFLECTIVE_RCE_CONFIG, 'curl'), ( 'ssrf_config', ANY_SSRF_CONFIG, hashlib.sha3_224(_SECRET.encode('utf-8')).hexdigest(), ), ) def test_generate_with_callback_returns_payload( self, config, expected_payload ): payload = self.payload_generator.generate(config) self.assertIn(expected_payload, payload.payload) self.assertIn(_IP_ADDRESS, payload.payload) self.assertIn(str(_PORT), payload.payload) self.assertTrue(payload.get_payload_attributes().uses_callback_server) @parameterized.named_parameters( ( 'linux_config', LINUX_REFLECTIVE_RCE_CONFIG, ( 'printf %s%s%s' ' TSUNAMI_PAYLOAD_START {secret} TSUNAMI_PAYLOAD_END' ).format(secret=_SECRET), ), ( 'ssrf_config', ANY_SSRF_CONFIG, 'http://public-firing-range.appspot.com/', ), ( 'java_config', JAVA_REFLECTIVE_RCE_CONFIG, ( 'String.format("%s%s%s", "TSUNAMI_PAYLOAD_START",' ' "{secret}", "TSUNAMI_PAYLOAD_END")' ).format(secret=_SECRET), ), ) def test_generate_no_callback_returns_payload( self, config, expected_payload ): payload = self.payload_generator.generate_no_callback(config) self.assertIn(expected_payload, payload.payload) self.assertFalse(payload.get_payload_attributes().uses_callback_server) @requests_mock.mock() def test_check_if_executed_linux_and_payload_exec_returns_true(self, mock): body = '{ "has_dns_interaction":false, "has_http_interaction":true}' mock.register_uri('GET', _URL, content=body.encode('utf-8')) payload = self.payload_generator.generate(LINUX_REFLECTIVE_RCE_CONFIG) self.assertTrue(payload.check_if_executed()) @requests_mock.mock() def test_check_if_executed_linux_and_payload_exec_with_dns_returns_true( self, mock): body = '{ "has_dns_interaction":true, "has_http_interaction":true}' mock.register_uri('GET', _URL, content=body.encode('utf-8')) payload = self.payload_generator.generate(LINUX_REFLECTIVE_RCE_CONFIG) self.assertTrue(payload.check_if_executed()) @requests_mock.mock() def test_check_if_executed_linux_no_payload_exec_returns_false(self, mock): body = '{ "has_dns_interaction":false, "has_http_interaction":false}' mock.register_uri('GET', _URL, content=body.encode('utf-8')) payload = self.payload_generator.generate(LINUX_REFLECTIVE_RCE_CONFIG) self.assertFalse(payload.check_if_executed()) @requests_mock.mock() def test_check_if_executed_ssrf_and_payload_exec_returns_true(self, mock): body = '{ "has_dns_interaction":true, "has_http_interaction":false}' mock.register_uri('GET', _URL, content=body.encode('utf-8')) payload = self.payload_generator.generate(ANY_SSRF_CONFIG) self.assertTrue(payload.check_if_executed()) @requests_mock.mock() def test_check_if_executed_ssrf_and_no_payload_exec_returns_false(self, mock): body = '{ "has_dns_interaction":false, "has_http_interaction":false}' mock.register_uri('GET', _URL, content=body.encode('utf-8')) payload = self.payload_generator.generate(ANY_SSRF_CONFIG) self.assertFalse(payload.check_if_executed()) def test_generate_with_no_vulnerability_type_raises_lookup_error(self): with self.assertRaises(LookupError) as exc: self.payload_generator.generate( pg.PayloadGeneratorConfig( interpretation_environment=pg.PayloadGeneratorConfig.InterpretationEnvironment.INTERPRETATION_ANY, execution_environment=pg.PayloadGeneratorConfig.ExecutionEnvironment.EXEC_INTERPRETATION_ENVIRONMENT, ) ) self.assertEqual( ( 'No payload implemented for VULNERABILITY_TYPE_UNSPECIFIED' ' vulnerability type, INTERPRETATION_ANY interpretation' ' environment, and EXEC_INTERPRETATION_ENVIRONMENT execution' ' environment.' ), str(exc.exception), ) def test_generate_with_no_interpretation_environment_raises_error(self): with self.assertRaises(LookupError) as exc: self.payload_generator.generate( pg.PayloadGeneratorConfig( vulnerability_type=pg.PayloadGeneratorConfig.VulnerabilityType.REFLECTIVE_RCE, execution_environment=pg.PayloadGeneratorConfig.ExecutionEnvironment.EXEC_INTERPRETATION_ENVIRONMENT, ) ) self.assertEqual( ( 'No payload implemented for REFLECTIVE_RCE vulnerability type,' ' INTERPRETATION_ENVIRONMENT_UNSPECIFIED interpretation' ' environment, and EXEC_INTERPRETATION_ENVIRONMENT execution' ' environment.' ), str(exc.exception), ) def test_generate_with_no_execution_environment_raises_lookup_error(self): with self.assertRaises(LookupError) as exc: self.payload_generator.generate( pg.PayloadGeneratorConfig( interpretation_environment=pg.PayloadGeneratorConfig.InterpretationEnvironment.INTERPRETATION_ANY, vulnerability_type=pg.PayloadGeneratorConfig.VulnerabilityType.REFLECTIVE_RCE, ) ) self.assertEqual( ( 'No payload implemented for REFLECTIVE_RCE vulnerability type,' ' INTERPRETATION_ANY interpretation environment, and' ' EXECUTION_ENVIRONMENT_UNSPECIFIED execution environment.' ), str(exc.exception), ) def test_generate_with_no_config_raises_lookup_error(self): with self.assertRaises(LookupError) as exc: self.payload_generator.generate(pg.PayloadGeneratorConfig()) self.assertEqual( ( 'No payload implemented for VULNERABILITY_TYPE_UNSPECIFIED' ' vulnerability type, INTERPRETATION_ENVIRONMENT_UNSPECIFIED' ' interpretation environment, and EXECUTION_ENVIRONMENT_UNSPECIFIED' ' execution environment.' ), str(exc.exception), ) class PayloadGeneratorWithoutCallbackTest(parameterized.TestCase): @classmethod def setUpClass(cls): super().setUpClass() psg = PayloadSecretGenerator() psg.generate = unittest.mock.MagicMock(return_value=_SECRET) payloads = get_parsed_payload() disabled_client = TcsClient('', 0, '', RequestsHttpClientBuilder().build()) cls.payload_generator_without_callback_server = PayloadGenerator( psg, payloads, disabled_client ) def test_is_callback_server_enabled_with_no_callback_returns_false(self): self.assertFalse( self.payload_generator_without_callback_server.is_callback_server_enabled() ) @parameterized.named_parameters( ( 'linux_config', LINUX_REFLECTIVE_RCE_CONFIG, ( 'printf %s%s%s TSUNAMI_' 'PAYLOAD_START {secret} TSUNAMI_PAYLOAD_END' ).format(secret=_SECRET), ), ( 'ssrf_config', ANY_SSRF_CONFIG, 'http://public-firing-range.appspot.com/', ), ( 'java_config', JAVA_REFLECTIVE_RCE_CONFIG, ( 'String.format("%s%s%s", "TSUNAMI_PAYLOAD_START",' ' "{secret}", "TSUNAMI_PAYLOAD_END")' ).format(secret=_SECRET), ), ) def test_generate_without_callback_returns_payload( self, config, expected_payload ): payload = self.payload_generator_without_callback_server.generate(config) self.assertEqual(expected_payload, payload.payload) self.assertFalse(payload.get_payload_attributes().uses_callback_server) @parameterized.named_parameters( ( 'linux_config', LINUX_REFLECTIVE_RCE_CONFIG, ('TSUNAMI_PAYLOAD_START{secret}TSUNAMI_PAYLOAD_END').format( secret=_SECRET ), ), ('ssrf_config', ANY_SSRF_CONFIG, '

What is the Firing Range?

'), ( 'java_config', JAVA_REFLECTIVE_RCE_CONFIG, ('TSUNAMI_PAYLOAD_START{secret}TSUNAMI_PAYLOAD_END').format( secret=_SECRET ), ), ) def test_check_if_executed_with_correct_payload_input_returns_true( self, config, expected_payload ): payload = self.payload_generator_without_callback_server.generate(config) payload_byte = bytes(expected_payload, 'utf-8') self.assertTrue(payload.check_if_executed(payload_byte)) @parameterized.named_parameters( ('linux_config', LINUX_REFLECTIVE_RCE_CONFIG, 'Random input'), ('ssrf_config', ANY_SSRF_CONFIG, '

Not Firing Range

'), ( 'java_config', JAVA_REFLECTIVE_RCE_CONFIG, 'TSUNAMI_PAYLOAD_START_Nothing_here_TSUNAMI_PAYLOAD_END', ), ) def test_check_if_executed_with_bad_payload_input_returns_false( self, config, expected_payload ): payload = self.payload_generator_without_callback_server.generate(config) payload_byte = bytes(expected_payload, 'utf-8') self.assertFalse(payload.check_if_executed(payload_byte)) def test_generate_with_no_callback_no_vulnerability_type_raises_error(self): with self.assertRaises(LookupError) as exc: self.payload_generator_without_callback_server.generate( pg.PayloadGeneratorConfig( interpretation_environment=pg.PayloadGeneratorConfig.InterpretationEnvironment.INTERPRETATION_ANY, execution_environment=pg.PayloadGeneratorConfig.ExecutionEnvironment.EXEC_INTERPRETATION_ENVIRONMENT, ) ) self.assertEqual( ( 'No payload implemented for VULNERABILITY_TYPE_UNSPECIFIED' ' vulnerability type, INTERPRETATION_ANY interpretation' ' environment, and EXEC_INTERPRETATION_ENVIRONMENT execution' ' environment.' ), str(exc.exception), ) def test_generate_with_no_callback_no_interpretation_env_raises_error(self): with self.assertRaises(LookupError) as exc: self.payload_generator_without_callback_server.generate( pg.PayloadGeneratorConfig( vulnerability_type=pg.PayloadGeneratorConfig.VulnerabilityType.REFLECTIVE_RCE, execution_environment=pg.PayloadGeneratorConfig.ExecutionEnvironment.EXEC_INTERPRETATION_ENVIRONMENT, ) ) self.assertEqual( ( 'No payload implemented for REFLECTIVE_RCE vulnerability type,' ' INTERPRETATION_ENVIRONMENT_UNSPECIFIED interpretation' ' environment, and EXEC_INTERPRETATION_ENVIRONMENT execution' ' environment.' ), str(exc.exception), ) def test_generate_with_no_callback_and_no_execution_env_raises_error(self): with self.assertRaises(LookupError) as exc: self.payload_generator_without_callback_server.generate( pg.PayloadGeneratorConfig( interpretation_environment=pg.PayloadGeneratorConfig.InterpretationEnvironment.INTERPRETATION_ANY, vulnerability_type=pg.PayloadGeneratorConfig.VulnerabilityType.REFLECTIVE_RCE, ) ) self.assertEqual( ( 'No payload implemented for REFLECTIVE_RCE vulnerability type,' ' INTERPRETATION_ANY interpretation environment, and' ' EXECUTION_ENVIRONMENT_UNSPECIFIED execution environment.' ), str(exc.exception), ) def test_generate_with_no_callback_and_no_config_raises_lookup_error(self): with self.assertRaises(LookupError) as exc: self.payload_generator_without_callback_server.generate( pg.PayloadGeneratorConfig() ) self.assertEqual( ( 'No payload implemented for VULNERABILITY_TYPE_UNSPECIFIED' ' vulnerability type, INTERPRETATION_ENVIRONMENT_UNSPECIFIED' ' interpretation environment, and EXECUTION_ENVIRONMENT_UNSPECIFIED' ' execution environment.' ), str(exc.exception), ) def test_generate_with_no_callback_and_no_validation_type_raises_error(self): payload_list = [pg.PayloadDefinition( interpretation_environment=pg.PayloadGeneratorConfig.InterpretationEnvironment.LINUX_SHELL, name=wrappers_pb2.StringValue(value='linux_printf'), execution_environment=pg.PayloadGeneratorConfig.ExecutionEnvironment.EXEC_INTERPRETATION_ENVIRONMENT, vulnerability_type=[pg.PayloadGeneratorConfig.VulnerabilityType.VULNERABILITY_TYPE_UNSPECIFIED], uses_callback_server=wrappers_pb2.BoolValue(value=False), payload_string=wrappers_pb2.StringValue(value='printf %s%s%s TSUNAMI_' 'PAYLOAD_START $TSUNAMI_PAYLOAD' '_TOKEN_RANDOM TSUNAMI_PAYLOAD_' 'END'), )] psg = PayloadSecretGenerator() psg.generate = unittest.mock.MagicMock(return_value=_SECRET) payload_generator_no_callback = PayloadGenerator( psg, payload_list, TcsClient('', 0, '', RequestsHttpClientBuilder().build()), ) with self.assertRaises(NotImplementedError) as exc: payload_generator_no_callback.generate(LINUX_UNSPECIFIED_CONFIG) self.assertEqual( 'Validation type VULNERABILITY_TYPE_UNSPECIFIED not supported.', str(exc.exception), ) def test_check_if_executed_with_no_callback_and_no_payload_raises_error(self): payload = self.payload_generator_without_callback_server.generate( LINUX_REFLECTIVE_RCE_CONFIG ) with self.assertRaises(ValueError) as exc: payload.check_if_executed() self.assertEqual('No valid payload input is entered.', str(exc.exception)) if __name__ == '__main__': absltest.main() ================================================ FILE: plugin_server/py/plugin/payload/payload_generator_test_helper.py ================================================ """Payload Generator Test Helper.""" import payload_generator_pb2 as pg LINUX_REFLECTIVE_RCE_CONFIG = pg.PayloadGeneratorConfig( vulnerability_type=pg.PayloadGeneratorConfig.VulnerabilityType.REFLECTIVE_RCE, interpretation_environment=pg.PayloadGeneratorConfig.InterpretationEnvironment.LINUX_SHELL, execution_environment=pg.PayloadGeneratorConfig.ExecutionEnvironment.EXEC_INTERPRETATION_ENVIRONMENT, ) LINUX_UNSPECIFIED_CONFIG = pg.PayloadGeneratorConfig( vulnerability_type=pg.PayloadGeneratorConfig.VulnerabilityType.VULNERABILITY_TYPE_UNSPECIFIED, interpretation_environment=pg.PayloadGeneratorConfig.InterpretationEnvironment.LINUX_SHELL, execution_environment=pg.PayloadGeneratorConfig.ExecutionEnvironment.EXEC_INTERPRETATION_ENVIRONMENT, ) ANY_SSRF_CONFIG = pg.PayloadGeneratorConfig( vulnerability_type=pg.PayloadGeneratorConfig.VulnerabilityType.SSRF, interpretation_environment=pg.PayloadGeneratorConfig.InterpretationEnvironment.INTERPRETATION_ANY, execution_environment=pg.PayloadGeneratorConfig.ExecutionEnvironment.EXEC_ANY, ) JAVA_REFLECTIVE_RCE_CONFIG = pg.PayloadGeneratorConfig( vulnerability_type=pg.PayloadGeneratorConfig.VulnerabilityType.REFLECTIVE_RCE, interpretation_environment=pg.PayloadGeneratorConfig.InterpretationEnvironment.JAVA, execution_environment=pg.PayloadGeneratorConfig.ExecutionEnvironment.EXEC_INTERPRETATION_ENVIRONMENT, ) ================================================ FILE: plugin_server/py/plugin/payload/payload_secret_generator.py ================================================ """Payload Secret Generator class.""" import base64 import os class PayloadSecretGenerator: """Payload Secret Generator creates a one-time secret.""" def generate(self, size: int) -> str: """Generate a random secret string with n bytes. Args: size: number of bytes. Returns: The decoded string. """ random_bytes = os.urandom(size) return base64.b16encode(random_bytes).decode() ================================================ FILE: plugin_server/py/plugin/payload/payload_secret_generator_test.py ================================================ """Tests for google3.third_party.java_src.tsunami.plugin_server.py.plugin.payload.payload_secret_generator.""" import base64 from google3.testing.pybase import googletest from plugin.payload.payload_secret_generator import PayloadSecretGenerator _SECRET_BYTE_SIZE = 8 class PayloadSecretGeneratorTest(googletest.TestCase): def test_generate_returns_secret_with_specified_size(self): secret = PayloadSecretGenerator().generate(_SECRET_BYTE_SIZE) self.assertLen(base64.b16decode(secret), _SECRET_BYTE_SIZE) if __name__ == "__main__": googletest.main() ================================================ FILE: plugin_server/py/plugin/payload/payload_test.py ================================================ """Tests for google3.third_party.java_src.tsunami.plugin_server.py.plugin.payload.payload.""" from typing import Optional from absl.testing import absltest from absl.testing import parameterized from plugin.payload.payload import Payload from plugin.payload.validator import Validator import payload_generator_pb2 as pg _CONFIG = pg.PayloadGeneratorConfig() _PAYLOAD_ATTRIBUTES = pg.PayloadAttributes() class PayloadTest(parameterized.TestCase): class MockValidator(Validator): was_called = False def is_executed(self, payload_data: Optional[bytes]): self.was_called = True return False def test_get_payload_returns_payload_string(self): payload = Payload( 'payload string', self.MockValidator(), _PAYLOAD_ATTRIBUTES, _CONFIG ) self.assertEqual('payload string', payload.get_payload()) @parameterized.named_parameters( ('with_no_payload_runs_validator', None), ('with_payload_string_runs_validator', 'payload string'), ('with_bytes_runs_validator', b'payload string'), ) def test_check_if_executed(self, param): validator = self.MockValidator() payload = Payload('payload string', validator, _PAYLOAD_ATTRIBUTES, _CONFIG) payload.check_if_executed(param) self.assertTrue(validator.was_called) def test_get_payload_attributes_returns_payload_attributes(self): validator = self.MockValidator() payload = Payload('payload string', validator, _PAYLOAD_ATTRIBUTES, _CONFIG) self.assertEqual(_PAYLOAD_ATTRIBUTES, payload.get_payload_attributes()) if __name__ == '__main__': absltest.main() ================================================ FILE: plugin_server/py/plugin/payload/payload_utility.py ================================================ """Utility service for retrieving and parsing payload.""" from ruamel import yaml from google.protobuf import json_format from pathlib import Path import payload_generator_pb2 as pg _PATH = '../../plugin/src/main/resources/com/google/tsunami/plugin/payload/payload_definitions.yaml' def set_payload_file_path(path: str): """Set the path to the payload file. Args: path: The path to the payload file. """ if not path: return global _PATH _PATH = path def get_parsed_payload() -> list[pg.PayloadDefinition]: """Get payload from payload_definitions.yaml. Returns: PayloadLibrary: List of payload definitions after validation. Raises: ValueError: If payload is missing a name, interpretation_environment, uses_callback_server, payload_string, or vulnerability_type. If the payload definition is invalid. Examples of invalidity include: - Payload that uses callback but the tsunami payload string is missing. - Payload that does not uses callback but has no specified validation type. - Payload that uses validation regex but does not specify the regex to be used. """ payload_str = Path(_PATH).read_text() yaml_parser = yaml.YAML(typ='safe', pure=True) payload_dict = yaml_parser.load(payload_str) payload_library = json_format.ParseDict(payload_dict, pg.PayloadLibrary()) return _validate_payloads([p for p in payload_library.payloads]) def _validate_payloads( payloads: list[pg.PayloadDefinition], ) -> list[pg.PayloadDefinition]: """Validate the pre-loaded payloads.""" for payload in payloads: if not payload.HasField('name'): raise ValueError('Parse payload does not have a name.') if ( payload.interpretation_environment is pg.PayloadGeneratorConfig.InterpretationEnvironment.INTERPRETATION_ENVIRONMENT_UNSPECIFIED ): raise ValueError( 'Parse payload does not have an interpretation environment.' ) if ( payload.execution_environment is pg.PayloadGeneratorConfig.ExecutionEnvironment.EXECUTION_ENVIRONMENT_UNSPECIFIED ): raise ValueError('Parse payload does not have an execution environment.') if ( pg.PayloadGeneratorConfig.VulnerabilityType.VULNERABILITY_TYPE_UNSPECIFIED in payload.vulnerability_type ): raise ValueError('Parse payload does not have a vulnerability type.') if not payload.HasField('payload_string'): raise ValueError('Parse payload does not have a payload string.') if bool( payload.uses_callback_server.ByteSize() ) and '$TSUNAMI_PAYLOAD_TOKEN_URL' not in str(payload.payload_string): raise ValueError( 'Parse payload uses callback server but $TSUNAMI_PAYLOAD_TOKEN_URL' ' not found in payload string.' ) if not bool(payload.uses_callback_server.ByteSize()): if ( payload.validation_type is pg.PayloadValidationType.VALIDATION_TYPE_UNSPECIFIED ): raise ValueError( 'Parse payload does not have a validation type and' ' does not use the callback server.' ) if ( payload.validation_type is pg.PayloadValidationType.VALIDATION_REGEX and not payload.HasField('validation_regex') ): raise ValueError( 'Parse payload has no validation regex but uses' ' PayloadValidationType.REGEX.' ) return payloads ================================================ FILE: plugin_server/py/plugin/payload/payload_utility_test.py ================================================ """Tests for google3.third_party.java_src.tsunami.plugin_server.py.plugin.payload.payload_utility.""" import unittest from google3.pyglib import resources from google3.testing.pybase import googletest from plugin.payload import payload_utility class PayloadUtilityTest(googletest.TestCase): def test_get_parsed_payload_with_good_payloads_returns_payloads(self): pd_string = """payloads: - name: The Good PD interpretation_environment: LINUX_SHELL execution_environment: EXEC_INTERPRETATION_ENVIRONMENT uses_callback_server: true payload_string: curl $TSUNAMI_PAYLOAD_TOKEN_URL vulnerability_type: - REFLECTIVE_RCE """ resources.GetResource = unittest.mock.Mock(return_value=pd_string) self.assertLen(payload_utility.get_parsed_payload(), 1) def test_get_parsed_payload_without_name_raise_valueerror(self): pd_string = """payloads: - interpretation_environment: LINUX_SHELL execution_environment: EXEC_INTERPRETATION_ENVIRONMENT uses_callback_server: true payload_string: curl $TSUNAMI_PAYLOAD_TOKEN_URL vulnerability_type: - REFLECTIVE_RCE """ with self.assertRaises(ValueError) as exc: resources.GetResource = unittest.mock.Mock(return_value=pd_string) payload_utility.get_parsed_payload() self.assertEqual('Parse payload does not have a name.', str(exc.exception)) def test_get_parsed_payload_no_interpretation_environment_raise_error(self): pd_string = """payloads: - name: The Good PD interpretation_environment: INTERPRETATION_ENVIRONMENT_UNSPECIFIED execution_environment: EXEC_INTERPRETATION_ENVIRONMENT uses_callback_server: true payload_string: curl $TSUNAMI_PAYLOAD_TOKEN_URL vulnerability_type: - REFLECTIVE_RCE """ with self.assertRaises(ValueError) as exc: resources.GetResource = unittest.mock.Mock(return_value=pd_string) payload_utility.get_parsed_payload() self.assertEqual( 'Parse payload does not have an interpretation environment.', str(exc.exception), ) def test_get_parsed_payload_no_execution_environment_raise_error(self): pd_string = """payloads: - name: The Good PD interpretation_environment: LINUX_SHELL execution_environment: EXECUTION_ENVIRONMENT_UNSPECIFIED uses_callback_server: true payload_string: curl $TSUNAMI_PAYLOAD_TOKEN_URL vulnerability_type: - REFLECTIVE_RCE """ with self.assertRaises(ValueError) as exc: resources.GetResource = unittest.mock.Mock(return_value=pd_string) payload_utility.get_parsed_payload() self.assertEqual( 'Parse payload does not have an execution environment.', str(exc.exception), ) def test_get_parsed_payload_no_vulnerability_type_raise_error(self): pd_string = """payloads: - name: The Good PD interpretation_environment: LINUX_SHELL execution_environment: EXEC_INTERPRETATION_ENVIRONMENT payload_string: curl $TSUNAMI_PAYLOAD_TOKEN_URL uses_callback_server: true vulnerability_type: - VULNERABILITY_TYPE_UNSPECIFIED """ with self.assertRaises(ValueError) as exc: resources.GetResource = unittest.mock.Mock(return_value=pd_string) payload_utility.get_parsed_payload() self.assertEqual( 'Parse payload does not have a vulnerability type.', str(exc.exception), ) def test_get_parsed_payload_no_payload_string_raise_error(self): pd_string = """payloads: - name: The Good PD interpretation_environment: LINUX_SHELL execution_environment: EXEC_INTERPRETATION_ENVIRONMENT uses_callback_server: true vulnerability_type: - REFLECTIVE_RCE """ with self.assertRaises(ValueError) as exc: resources.GetResource = unittest.mock.Mock(return_value=pd_string) payload_utility.get_parsed_payload() self.assertEqual( 'Parse payload does not have a payload string.', str(exc.exception), ) def test_get_parsed_payload_no_callback_constant_in_payload_raise_error(self): pd_string = """payloads: - name: The Good PD interpretation_environment: LINUX_SHELL execution_environment: EXEC_INTERPRETATION_ENVIRONMENT uses_callback_server: true payload_string: curl soemthing here vulnerability_type: - REFLECTIVE_RCE """ with self.assertRaises(ValueError) as exc: resources.GetResource = unittest.mock.Mock(return_value=pd_string) payload_utility.get_parsed_payload() self.assertEqual( ( 'Parse payload uses callback server but $TSUNAMI_PAYLOAD_TOKEN_URL' ' not found in payload string.' ), str(exc.exception), ) def test_get_parsed_payload_no_callback_server_no_validation_type_raise_error( self): pd_string = """payloads: - name: The Good PD interpretation_environment: LINUX_SHELL execution_environment: EXEC_INTERPRETATION_ENVIRONMENT uses_callback_server: false payload_string: curl $TSUNAMI_PAYLOAD_TOKEN_URL validation_type: VALIDATION_TYPE_UNSPECIFIED vulnerability_type: - REFLECTIVE_RCE """ with self.assertRaises(ValueError) as exc: resources.GetResource = unittest.mock.Mock(return_value=pd_string) payload_utility.get_parsed_payload() self.assertEqual( ( 'Parse payload does not have a validation type and does not use the' ' callback server.' ), str(exc.exception), ) def test_get_parsed_payload_no_callback_server_no_val_regex_raise_error( self): pd_string = """payloads: - name: The Good PD interpretation_environment: LINUX_SHELL execution_environment: EXEC_INTERPRETATION_ENVIRONMENT uses_callback_server: false payload_string: curl $TSUNAMI_PAYLOAD_TOKEN_URL validation_type: VALIDATION_REGEX vulnerability_type: - REFLECTIVE_RCE """ with self.assertRaises(ValueError) as exc: resources.GetResource = unittest.mock.Mock(return_value=pd_string) payload_utility.get_parsed_payload() self.assertEqual( ( 'Parse payload has no validation regex but uses' ' PayloadValidationType.REGEX.' ), str(exc.exception), ) if __name__ == '__main__': googletest.main() ================================================ FILE: plugin_server/py/plugin/payload/validator.py ================================================ """Interface type for the function to verify payload execution.""" import abc from typing import Optional class Validator(metaclass=abc.ABCMeta): """Type used to verify payload execution in the out-of-bound Payload.""" @abc.abstractmethod def is_executed(self, data: Optional[bytes]) -> bool: """Checks whether the payload is executed. Args: data: Optional data in bytes representation. Returns: Whether the payload is executed. """ ================================================ FILE: plugin_server/py/plugin/tcs_client.py ================================================ """Tsunami Callback Server client.""" import hashlib from typing import Optional from absl import logging from google.protobuf import json_format from common.data import network_endpoint_utils from common.net.http.http_client import HttpClient from common.net.http.http_headers import HttpHeaders from common.net.http.http_request import HttpRequest import network_pb2 import polling_pb2 class TcsClient: """Client use for communicating with Tsunami Callback Server.""" def __init__( self, callback_address: str, callback_port: int, polling_base_url: str, http_client: HttpClient, ): """Initialize Tsunami Callback Server client. Args: callback_address: IP address or the hostname of the callback server. callback_port: The port that the callback server is running on. polling_base_url: Base url of the callback server. http_client: Client used to send HTTP requests and process responses. """ self.callback_endpoint = self._create_callback_address( callback_address, callback_port ) self.polling_base_url = self._remove_trailing_slashes(polling_base_url) self.http_client = http_client def is_callback_server_enabled(self) -> bool: """Check if callback server is enabled. Returns: Whether callback server is reachable. """ return bool( self.callback_endpoint.ip_address.address or self.callback_endpoint.hostname.name ) and bool(self.polling_base_url) def get_callback_uri(self, secret_string: str) -> str: """Assemble the URI to reach the callback server. Args: secret_string: Callback unique ID that is bonded to the scan target. Used to include in the request URI. Returns: The server URI. Examples with provided hostname: 04041e8898e739ca33.google.com 04041e8898e739ca33.google.com:8080 Examples with provided IP address: http://127.0.0.1:8080/04041e8898e739ca33 http://[2001:db8:3333:4444:5555:6666:7777:8888]/04041e8898e739ca33 """ # Generate hash from 8 bytes secret string using SHA-3 hashing cbid = hashlib.sha3_224(secret_string.encode('utf-8')).hexdigest() uri = network_endpoint_utils.to_uri_authority(self.callback_endpoint) # return uri with provided hostname if network_endpoint_utils.has_hostname(self.callback_endpoint): return '%s.%s' % (cbid, uri) # return uri with provided ip address return 'http://%s/%s' % (uri, cbid) def has_oob_log(self, secret_string: str) -> bool: """Check if callback server has received OOB log. Args: secret_string: Callback unique ID that is bonded to the scan target. Used to include in the request URI. Returns: If callback server has out-of-bounds log. """ result = self._send_polling_request(secret_string) if result: return result.has_dns_interaction or result.has_http_interaction return False def _send_polling_request( self, secret_string: str ) -> Optional[polling_pb2.PollingResult]: """Send HTTP requests to the callback server. Args: secret_string: Callback unique ID that is bonded to the scan target. Used to include in the request URI. Returns: The polling results of whether or not the scan target has DNS or HTTP interaction. Returns None if polling request failed. """ request = self._build_polling_request(secret_string) try: response = self.http_client.send(request) if response.status and response.status.is_success(): return json_format.Parse( response.body_string(), polling_pb2.PollingResult() ) else: code = response.status.code if response.status else 'UNKNOWN' logging.info('OOB server returned %s.', code) except (json_format.ParseError, ValueError): logging.exception('Polling request failed.') return None def _build_polling_request(self, secret_string: str) -> HttpRequest: url = '%s/?secret=%s' % (self.polling_base_url, secret_string) return ( HttpRequest.get(url) .set_headers( HttpHeaders.builder() .add_header('Cache-Control', 'no-cache') .build() ) .build() ) def _create_callback_address( self, address: str, port: int ) -> network_pb2.NetworkEndpoint: try: return ( network_endpoint_utils.for_ip(address) if port == 80 else network_endpoint_utils.for_ip_and_port(address, port) ) except ValueError: return ( network_endpoint_utils.for_hostname(address) if port == 80 else network_endpoint_utils.for_hostname_and_port(address, port) ) def _remove_trailing_slashes(self, url: str) -> str: return url.strip('/') ================================================ FILE: plugin_server/py/plugin/tcs_client_test.py ================================================ """Tests for google3.third_party.java_src.tsunami.plugin_server.py.plugin.tcs_client.""" from absl.testing import absltest from absl.testing import parameterized import requests_mock from common.net.http.requests_http_client import RequestsHttpClientBuilder from plugin.tcs_client import TcsClient SECRET = 'a3d9ed89deadbeef' CBID = '04041e8898e739ca33a250923e24f59ca41a8373f8cf6a45a1275f3b' IPV4 = '127.0.0.1' IPV6 = '2001:0db8:85a3:0000:0000:8a2e:0370:7334' PORT = 8000 DOMAIN = 'valid.com' URL = 'http://valid.com' INVALID_ADDRESS = 'http://invalid.com' class TcsClientTest(parameterized.TestCase): @classmethod def setUpClass(cls): super().setUpClass() cls.http_client = RequestsHttpClientBuilder().build() @parameterized.named_parameters( ( 'with_valid_ipv4_returns_uri', IPV4, PORT, 'http://%s:%s/%s' % (IPV4, PORT, CBID), ), ( 'with_valid_ipv6_returns_uri', IPV6, PORT, 'http://[%s]:%s/%s' % (IPV6, PORT, CBID), ), ( 'with_valid_domain_returns_uri', DOMAIN, PORT, '%s.%s:%s' % (CBID, DOMAIN, PORT), ), ( 'with_port_80_and_ip_returns_uri_without_port', IPV4, 80, 'http://%s/%s' % (IPV4, CBID), ), ( 'with_port_80_and_domain_returns_uri_without_port', DOMAIN, 80, '%s.%s' % (CBID, DOMAIN), ), ) def test_get_callback_uri(self, address, port, exepected_uri): client = TcsClient(address, port, URL, self.http_client) resulted_uri = client.get_callback_uri(SECRET) self.assertEqual(resulted_uri, exepected_uri) @parameterized.named_parameters( ('with_valid_ipv4_returns_true', IPV4, URL, True), ('with_valid_ipv6_returns_true', IPV6, URL, True), ('with_valid_hostname_and_base_url_returns_true', DOMAIN, URL, True), ('with_invalid_hostname_returns_false', '', URL, False), ('with_invalid_base_url_returns_false', DOMAIN, '', False), ) def test_is_callback_server_enabled( self, address, url, expected ): client = TcsClient(address, PORT, url, self.http_client) self.assertEqual(client.is_callback_server_enabled(), expected) def test_tcs_client_with_invalid_port_raises_error(self): with self.assertRaises(ValueError): TcsClient(DOMAIN, 100000, URL, self.http_client) @requests_mock.mock() def test_has_oob_log_sends_polling_request(self, mock): body = '{ "has_dns_interaction":false, "has_http_interaction":true}' mock.register_uri( 'GET', '%s/?secret=%s' % (URL, SECRET), content=body.encode('utf-8') ) client = TcsClient(DOMAIN, PORT, URL, self.http_client) self.assertTrue(client.has_oob_log(SECRET)) @requests_mock.mock() def test_has_oob_log_with_no_logs_returns_false(self, mock): body = '{ "has_dns_interaction":false, "has_http_interaction":false}' mock.register_uri( 'GET', '%s/?secret=%s' % (URL, SECRET), content=body.encode('utf-8') ) client = TcsClient(DOMAIN, PORT, URL, self.http_client) self.assertFalse(client.has_oob_log(SECRET)) @requests_mock.mock() def test_has_oob_log_with_no_response_returns_false(self, mock): mock.register_uri('GET', '%s/?secret=%s' % (URL, SECRET)) client = TcsClient(DOMAIN, PORT, URL, self.http_client) self.assertFalse(client.has_oob_log(SECRET)) @requests_mock.mock() def test_has_oob_log_with_unsuccessful_response_returns_false(self, mock): mock.register_uri('GET', '%s/?secret=%s' % (URL, SECRET), status_code=500) client = TcsClient(DOMAIN, PORT, URL, self.http_client) self.assertFalse(client.has_oob_log(SECRET)) def test_create_tscclient_removes_trailing_slashes(self): client = TcsClient(DOMAIN, PORT, 'http://my-url.com//', self.http_client) self.assertEqual('http://my-url.com', client.polling_base_url) if __name__ == '__main__': absltest.main() ================================================ FILE: plugin_server/py/plugin_server.py ================================================ # Copyright 2022 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Main gRPC server to execute Python Tsunami plugins.""" from concurrent import futures import importlib import pkgutil import signal import threading import types from absl import app from absl import flags from absl import logging import grpc from grpc_health.v1 import health from grpc_health.v1 import health_pb2 from grpc_health.v1 import health_pb2_grpc from grpc_reflection.v1alpha import reflection import plugin_service import tsunami_plugin from common.net.http.requests_http_client import RequestsHttpClientBuilder from plugin.payload import payload_utility from plugin.payload.payload_generator import PayloadGenerator from plugin.payload.payload_secret_generator import PayloadSecretGenerator from plugin.payload.payload_utility import get_parsed_payload from plugin.tcs_client import TcsClient import plugin_service_pb2 import plugin_service_pb2_grpc _HOST = '127.0.0.1' _PORT = flags.DEFINE_integer('port', 34567, 'port to listen on.') _THREADS = flags.DEFINE_integer('threads', 10, 'number of worker threads in thread pool.') _OUTPUT = flags.DEFINE_string('log_output', '/tmp', 'server execution log directory.') _TIMEOUT_SEC = flags.DEFINE_float( 'timeout_seconds', 10, 'Timeout in seconds for complete HTTP calls.' ) _LOG_ID = flags.DEFINE_string('log_id', '', 'id to track logs for all outgoing HTTP calls.') _TRUST_ALL_SSL_CERT = flags.DEFINE_boolean( 'trust_all_ssl_cert', True, 'Trust all SSL certificates on HTTPS traffic.' ) _CALLBACK_ADDRESS = flags.DEFINE_string( 'callback_address', '127.0.0.1', 'Hostname or IP address of the callback server.', ) _CALLBACK_PORT = flags.DEFINE_integer( 'callback_port', 8881, 'Callback server port for HTTP logging service.' ) _CALLBACK_POLLING_URI = flags.DEFINE_string( 'polling_uri', 'http://127.0.0.1:8880', 'Callback server URI for log polling service.', ) _PAYLOAD_FILE_PATH = flags.DEFINE_string( 'payload_file_path', '', 'Path to the payload_definitions.yaml file.', ) def main(unused_argv): _configure_log() payload_utility.set_payload_file_path(_PAYLOAD_FILE_PATH.value) # Load plugins from tsunami_plugins repository. plugin_pkg = importlib.import_module( 'py_plugins' ) _import_py_plugins(plugin_pkg) server_addr = f'{_HOST}:{_PORT.value}' server = grpc.server(futures.ThreadPoolExecutor(max_workers=_THREADS.value)) _configure_plugin_service(server) health_servicer = _register_health_service(server) server.add_insecure_port(server_addr) server.start() logging.info('Server started at %s.', server_addr) # Set to SERVING after server proves its availability. _set_health_service_to_serving(server, health_servicer) # Java Process.destroy() sends SIGTERM sig_term_received = threading.Event() def on_sigterm(signum, frame): logging.info('Got signal %s, %s', signum, frame) sig_term_received.set() signal.signal(signal.SIGTERM, on_sigterm) sig_term_received.wait() logging.info('Stopped RPC server, Waiting for RPCs to complete...') server.stop(3).wait() logging.info('Done stopping server') def _import_py_plugins(plugin_pkg: types.ModuleType): """Imports all Python Tsunami plugin modules.""" for _, name, is_pkg in pkgutil.walk_packages(plugin_pkg.__path__): full_name = plugin_pkg.__name__ + '.' + name pkg = importlib.import_module(full_name) if is_pkg: _import_py_plugins(pkg) else: logging.info('Loaded plugin module %s', full_name) def _configure_log(): """Store and print out log stream.""" logging.use_absl_handler() logger = logging.get_absl_handler() logger.use_absl_log_file( 'py_plugin_server', _OUTPUT.value ) # Label the log record that comes from the python server for debugging. logger.setFormatter( logging.PythonFormatter('[Python Server] %(asctime)s %(message)s') ) logging.set_verbosity(logging.INFO) def _configure_plugin_service(server): """Configures the main plugin service for handling plugin related gRPC requests.""" http_client = ( RequestsHttpClientBuilder() .set_timeout_sec(_TIMEOUT_SEC.value) .set_verify_ssl(not _TRUST_ALL_SSL_CERT.value) .set_log_id(_LOG_ID.value) .build() ) callback_client = TcsClient( _CALLBACK_ADDRESS.value, _CALLBACK_PORT.value, _CALLBACK_POLLING_URI.value, http_client, ) payload_generator = PayloadGenerator( PayloadSecretGenerator(), get_parsed_payload(), callback_client ) # Get all VulnDetector class implementations. plugins = [ cls(http_client, payload_generator) for cls in tsunami_plugin.VulnDetector.__subclasses__() ] logging.info('Configured %d python plugin:', len(plugins)) for plugin in plugins: logging.info('\t%s', plugin.GetPluginDefinition().info.name) servicer = plugin_service.PluginServiceServicer( py_plugins=plugins, max_workers=_THREADS.value ) plugin_service_pb2_grpc.add_PluginServiceServicer_to_server(servicer, server) def _register_health_service(server): """Add gRPC health checking service to server.""" health_servicer = health.HealthServicer( experimental_non_blocking=True, experimental_thread_pool=futures.ThreadPoolExecutor( max_workers=_THREADS.value)) health_pb2_grpc.add_HealthServicer_to_server(health_servicer, server) return health_servicer def _set_health_service_to_serving(server, health_servicer): """Set gRPC health checking service to SERVING.""" # Set all services to SERVING. services = tuple(service.full_name for service in plugin_service_pb2.DESCRIPTOR.services_by_name .values()) + (reflection.SERVICE_NAME, health.SERVICE_NAME) for service in services: health_servicer.set(service, health_pb2.HealthCheckResponse.SERVING) logging.info('Service %s is now SERVING', service) reflection.enable_server_reflection(services, server) if __name__ == '__main__': flags.set_default(logging.ALSOLOGTOSTDERR, True) app.run(main) ================================================ FILE: plugin_server/py/plugin_service.py ================================================ # Copyright 2022 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Python gRPC PluginService server adapter to provide communication with the Java client.""" from concurrent import futures from typing import cast from absl import logging import tsunami_plugin import detection_pb2 import plugin_representation_pb2 import plugin_service_pb2 import plugin_service_pb2_grpc RunResponse = plugin_service_pb2.RunResponse ListPluginsRequest = plugin_service_pb2.ListPluginsRequest ListPluginsResponse = plugin_service_pb2.ListPluginsResponse _PluginServiceServicer = plugin_service_pb2_grpc.PluginServiceServicer _PluginType = plugin_representation_pb2.PluginInfo.PluginType _DETECTION_TIMEOUT = 60 class PluginServiceServicer(plugin_service_pb2_grpc.PluginServiceServicer): """PluginService server implementation for communication with the Java client. This class executes requests called by the Java client. All request types are given by the plugin_service proto definition. """ def __init__(self, py_plugins: list[tsunami_plugin.TsunamiPlugin], max_workers: int): self.py_plugins = py_plugins self.max_workers = max_workers def Run( self, request: plugin_service_pb2.RunRequest, servicer_context: plugin_service_pb2_grpc.PluginServiceServicer, ) -> RunResponse: logging.info('Received Run request: %s', request) report_list = detection_pb2.DetectionReportList() detection_futures = [] with futures.ThreadPoolExecutor(max_workers=self.max_workers) as executor: for matched_plugin in request.plugins: plugin_def = matched_plugin.plugin for plugin in self.py_plugins: if plugin.GetPluginDefinition() == plugin_def: logging.info('Running python plugin %s.', type(plugin).__name__) if plugin_def.info.type is _PluginType.VULN_DETECTION: plugin = cast(tsunami_plugin.VulnDetector, plugin) detection_futures.append( executor.submit(plugin.Detect, request.target, matched_plugin.services)) for detection in detection_futures: report_list.detection_reports.extend( detection.result(timeout=_DETECTION_TIMEOUT).detection_reports) response = RunResponse() response.reports.CopyFrom(report_list) return response def ListPlugins( self, request: ListPluginsRequest, servicer_context: _PluginServiceServicer, ) -> ListPluginsResponse: logging.info('Received ListPlugins request: %s', request) response = ListPluginsResponse() response.plugins.MergeFrom( [plugin.GetPluginDefinition() for plugin in self.py_plugins]) return response ================================================ FILE: plugin_server/py/plugin_service_test.py ================================================ # Copyright 2022 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Tests for plugin_service.""" import ipaddress import time from absl.testing import absltest import grpc_testing from google.protobuf import timestamp_pb2 import plugin_service import tsunami_plugin from common.net.http.http_client import HttpClient from common.net.http.requests_http_client import RequestsHttpClientBuilder from plugin.payload.payload_generator import PayloadGenerator from plugin.payload.payload_secret_generator import PayloadSecretGenerator from plugin.payload.payload_utility import get_parsed_payload from plugin.tcs_client import TcsClient import detection_pb2 import network_pb2 import network_service_pb2 import plugin_representation_pb2 import plugin_service_pb2 import reconnaissance_pb2 import vulnerability_pb2 _NetworkEndpoint = network_pb2.NetworkEndpoint _NetworkService = network_service_pb2.NetworkService _PluginInfo = plugin_representation_pb2.PluginInfo _TargetInfo = reconnaissance_pb2.TargetInfo _AddressFamily = network_pb2.AddressFamily _ServiceDescriptor = plugin_service_pb2.DESCRIPTOR.services_by_name[ 'PluginService' ] _RunMethod = _ServiceDescriptor.methods_by_name['Run'] _ListPluginsMethod = _ServiceDescriptor.methods_by_name['ListPlugins'] MAX_WORKERS = 1 class PluginServiceTest(absltest.TestCase): def setUp(self): super().setUp() # payload generator and client setup self.request_client = RequestsHttpClientBuilder().build() psg = PayloadSecretGenerator() callback_client = TcsClient( '127.0.0.1', 8000, 'http://127.0.0.1:8000/test', self.request_client ) self.payload_generator = PayloadGenerator( psg, get_parsed_payload(), callback_client ) self.test_plugin = FakeVulnDetector( self.request_client, self.payload_generator ) self._time = grpc_testing.strict_fake_time(time.time()) self._server = grpc_testing.server_from_dictionary( { _ServiceDescriptor: plugin_service.PluginServiceServicer( py_plugins=[self.test_plugin], max_workers=MAX_WORKERS ), }, self._time, ) self._channel = grpc_testing.channel( plugin_service_pb2.DESCRIPTOR.services_by_name.values(), self._time ) def tearDown(self): self._channel.close() super().tearDown() def test_run_plugins_registered_returns_valid_response(self): plugin_to_test = FakeVulnDetector( self.request_client, self.payload_generator ) endpoint = _build_network_endpoint('1.1.1.1', 80) service = _NetworkService( network_endpoint=endpoint, transport_protocol=network_pb2.TCP, service_name='http', ) target = _TargetInfo(network_endpoints=[endpoint]) services = [service] request = plugin_service_pb2.RunRequest( target=target, plugins=[ plugin_service_pb2.MatchedPlugin( services=services, plugin=plugin_to_test.GetPluginDefinition() ) ], ) rpc = self._server.invoke_unary_unary(_RunMethod, (), request, None) response, _, _, _ = rpc.termination() self.assertLen(response.reports.detection_reports, 1) self.assertEqual( plugin_to_test._BuildFakeDetectionReport( target=target, network_service=services[0] ), response.reports.detection_reports[0], ) def test_run_no_plugins_registered_returns_empty_response(self): endpoint = _build_network_endpoint('1.1.1.1', 80) target = _TargetInfo(network_endpoints=[endpoint]) request = plugin_service_pb2.RunRequest(target=target, plugins=[]) rpc = self._server.invoke_unary_unary(_RunMethod, (), request, None) response, _, _, _ = rpc.termination() self.assertEmpty(response.reports.detection_reports) def test_list_plugins_plugins_registered_returns_valid_response(self): request = plugin_service.ListPluginsRequest() rpc = self._server.invoke_unary_unary(_ListPluginsMethod, (), request, None) response, _, _, _ = rpc.termination() self.assertEqual( plugin_service.ListPluginsResponse( plugins=[self.test_plugin.GetPluginDefinition()] ), response, ) def _build_network_endpoint(ip: str, port: int) -> _NetworkEndpoint: return _NetworkEndpoint( type=_NetworkEndpoint.IP, ip_address=network_pb2.IpAddress(address_family=_get_address_family(ip)), port=network_pb2.Port(port_number=port), ) def _get_address_family(ip: str) -> _AddressFamily: inet_addr = ipaddress.ip_address(ip) if inet_addr.version == 4: return _AddressFamily.IPV4 elif inet_addr.version == 6: return _AddressFamily.IPV6 else: raise ValueError("Unknown IP address family for IP '%s'" % ip) class FakeVulnDetector(tsunami_plugin.VulnDetector): """Fake Vulnerability detector class for testing only.""" def __init__( self, http_client: HttpClient, payload_generator: PayloadGenerator, ): self.http_client = http_client self.payload_generator = payload_generator def GetAdvisories(self) -> list[vulnerability_pb2.Vulnerability]: """Returns the advisories for this plugin.""" return [ vulnerability_pb2.Vulnerability( main_id=vulnerability_pb2.VulnerabilityId( publisher='GOOGLE', value='FakeVuln1' ), severity=vulnerability_pb2.CRITICAL, title='FakeTitle1', description='FakeDescription1', ), ] def GetPluginDefinition(self): return tsunami_plugin.PluginDefinition( info=_PluginInfo( type=_PluginInfo.VULN_DETECTION, name='fake', version='v0.1', description='fake description', author='fake author', ), target_service_name=plugin_representation_pb2.TargetServiceName( value=['fake service'] ), target_software=plugin_representation_pb2.TargetSoftware( name='fake software' ), for_web_service=False, ) def Detect(self, target, matched_services): return detection_pb2.DetectionReportList( detection_reports=[ self._BuildFakeDetectionReport(target, matched_services[0]) ] ) def _BuildFakeDetectionReport(self, target, network_service): return detection_pb2.DetectionReport( target_info=target, network_service=network_service, detection_timestamp=timestamp_pb2.Timestamp(nanos=1234567890), detection_status=detection_pb2.VULNERABILITY_VERIFIED, vulnerability=self.GetAdvisories()[0], ) # TODO(b/239628051): Add a failed VulnDetector class to test failed cases. if __name__ == '__main__': absltest.main() ================================================ FILE: plugin_server/py/requirements.in ================================================ absl-py==2.1.0 aenum==3.1.15 certifi==2024.7.4 charset-normalizer==3.3.2 glog==0.3.1 grpcio==1.63.0 grpcio-health-checking==1.63.0 grpcio-reflection==1.63.0 grpcio-tools==1.63.0 idna==3.7 protobuf==5.29.6 python-gflags==3.1.2 pyzmq==27.0.1 requests==2.32.4 requests-mock==1.12.1 ruamel.yaml==0.18.6 ruamel.yaml.clib==0.2.8 six==1.16.0 urllib3==2.6.3 ================================================ FILE: plugin_server/py/requirements.txt ================================================ # # This file is autogenerated by pip-compile with Python 3.12 # by the following command: # # pip-compile --allow-unsafe --generate-hashes requirements.in # absl-py==2.1.0 \ --hash=sha256:526a04eadab8b4ee719ce68f204172ead1027549089702d99b9059f129ff1308 \ --hash=sha256:7820790efbb316739cde8b4e19357243fc3608a152024288513dd968d7d959ff # via -r requirements.in aenum==3.1.15 \ --hash=sha256:27b1710b9d084de6e2e695dab78fe9f269de924b51ae2850170ee7e1ca6288a5 \ --hash=sha256:8cbd76cd18c4f870ff39b24284d3ea028fbe8731a58df3aa581e434c575b9559 \ --hash=sha256:e0dfaeea4c2bd362144b87377e2c61d91958c5ed0b4daf89cb6f45ae23af6288 # via -r requirements.in certifi==2024.7.4 \ --hash=sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b \ --hash=sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90 # via # -r requirements.in # requests charset-normalizer==3.3.2 \ --hash=sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027 \ --hash=sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087 \ --hash=sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786 \ --hash=sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8 \ --hash=sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09 \ --hash=sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185 \ --hash=sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574 \ --hash=sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e \ --hash=sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519 \ --hash=sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898 \ --hash=sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269 \ --hash=sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3 \ --hash=sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f \ --hash=sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6 \ --hash=sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8 \ --hash=sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a \ --hash=sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73 \ --hash=sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc \ --hash=sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714 \ --hash=sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2 \ --hash=sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc \ --hash=sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce \ --hash=sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d \ --hash=sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e \ --hash=sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6 \ --hash=sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269 \ --hash=sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96 \ --hash=sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d \ --hash=sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a \ --hash=sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4 \ --hash=sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77 \ --hash=sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d \ --hash=sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0 \ --hash=sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed \ --hash=sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068 \ --hash=sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac \ --hash=sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25 \ --hash=sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8 \ --hash=sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab \ --hash=sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26 \ --hash=sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2 \ --hash=sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db \ --hash=sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f \ --hash=sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5 \ --hash=sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99 \ --hash=sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c \ --hash=sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d \ --hash=sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811 \ --hash=sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa \ --hash=sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a \ --hash=sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03 \ --hash=sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b \ --hash=sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04 \ --hash=sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c \ --hash=sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001 \ --hash=sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458 \ --hash=sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389 \ --hash=sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99 \ --hash=sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985 \ --hash=sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537 \ --hash=sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238 \ --hash=sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f \ --hash=sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d \ --hash=sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796 \ --hash=sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a \ --hash=sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143 \ --hash=sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8 \ --hash=sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c \ --hash=sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5 \ --hash=sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5 \ --hash=sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711 \ --hash=sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4 \ --hash=sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6 \ --hash=sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c \ --hash=sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7 \ --hash=sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4 \ --hash=sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b \ --hash=sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae \ --hash=sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12 \ --hash=sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c \ --hash=sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae \ --hash=sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8 \ --hash=sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887 \ --hash=sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b \ --hash=sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4 \ --hash=sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f \ --hash=sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5 \ --hash=sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33 \ --hash=sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519 \ --hash=sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561 # via # -r requirements.in # requests glog==0.3.1 \ --hash=sha256:88cee83dea8bddf73db7edbf5bd697237628389ef476c0a0ecad639c606189e5 \ --hash=sha256:b721edef6009eabc0b4d9f2619e153d2627a7b71a3657c8ed69f02ef7c78be97 # via -r requirements.in grpcio==1.63.0 \ --hash=sha256:01799e8649f9e94ba7db1aeb3452188048b0019dc37696b0f5ce212c87c560c3 \ --hash=sha256:0697563d1d84d6985e40ec5ec596ff41b52abb3fd91ec240e8cb44a63b895094 \ --hash=sha256:08e1559fd3b3b4468486b26b0af64a3904a8dbc78d8d936af9c1cf9636eb3e8b \ --hash=sha256:166e5c460e5d7d4656ff9e63b13e1f6029b122104c1633d5f37eaea348d7356d \ --hash=sha256:1ff737cf29b5b801619f10e59b581869e32f400159e8b12d7a97e7e3bdeee6a2 \ --hash=sha256:219bb1848cd2c90348c79ed0a6b0ea51866bc7e72fa6e205e459fedab5770172 \ --hash=sha256:259e11932230d70ef24a21b9fb5bb947eb4703f57865a404054400ee92f42f5d \ --hash=sha256:2e93aca840c29d4ab5db93f94ed0a0ca899e241f2e8aec6334ab3575dc46125c \ --hash=sha256:3a6d1f9ea965e750db7b4ee6f9fdef5fdf135abe8a249e75d84b0a3e0c668a1b \ --hash=sha256:50344663068041b34a992c19c600236e7abb42d6ec32567916b87b4c8b8833b3 \ --hash=sha256:56cdf96ff82e3cc90dbe8bac260352993f23e8e256e063c327b6cf9c88daf7a9 \ --hash=sha256:5c039ef01516039fa39da8a8a43a95b64e288f79f42a17e6c2904a02a319b357 \ --hash=sha256:6426e1fb92d006e47476d42b8f240c1d916a6d4423c5258ccc5b105e43438f61 \ --hash=sha256:65bf975639a1f93bee63ca60d2e4951f1b543f498d581869922910a476ead2f5 \ --hash=sha256:6a1a3642d76f887aa4009d92f71eb37809abceb3b7b5a1eec9c554a246f20e3a \ --hash=sha256:6ef0ad92873672a2a3767cb827b64741c363ebaa27e7f21659e4e31f4d750280 \ --hash=sha256:756fed02dacd24e8f488f295a913f250b56b98fb793f41d5b2de6c44fb762434 \ --hash=sha256:75f701ff645858a2b16bc8c9fc68af215a8bb2d5a9b647448129de6e85d52bce \ --hash=sha256:8064d986d3a64ba21e498b9a376cbc5d6ab2e8ab0e288d39f266f0fca169b90d \ --hash=sha256:878b1d88d0137df60e6b09b74cdb73db123f9579232c8456f53e9abc4f62eb3c \ --hash=sha256:8f3f6883ce54a7a5f47db43289a0a4c776487912de1a0e2cc83fdaec9685cc9f \ --hash=sha256:91b73d3f1340fefa1e1716c8c1ec9930c676d6b10a3513ab6c26004cb02d8b3f \ --hash=sha256:93a46794cc96c3a674cdfb59ef9ce84d46185fe9421baf2268ccb556f8f81f57 \ --hash=sha256:93f45f27f516548e23e4ec3fbab21b060416007dbe768a111fc4611464cc773f \ --hash=sha256:9e350cb096e5c67832e9b6e018cf8a0d2a53b2a958f6251615173165269a91b0 \ --hash=sha256:a2d60cd1d58817bc5985fae6168d8b5655c4981d448d0f5b6194bbcc038090d2 \ --hash=sha256:a3abfe0b0f6798dedd2e9e92e881d9acd0fdb62ae27dcbbfa7654a57e24060c0 \ --hash=sha256:a44624aad77bf8ca198c55af811fd28f2b3eaf0a50ec5b57b06c034416ef2d0a \ --hash=sha256:a7b19dfc74d0be7032ca1eda0ed545e582ee46cd65c162f9e9fc6b26ef827dc6 \ --hash=sha256:ad2ac8903b2eae071055a927ef74121ed52d69468e91d9bcbd028bd0e554be6d \ --hash=sha256:b005292369d9c1f80bf70c1db1c17c6c342da7576f1c689e8eee4fb0c256af85 \ --hash=sha256:b2e44f59316716532a993ca2966636df6fbe7be4ab6f099de6815570ebe4383a \ --hash=sha256:b3afbd9d6827fa6f475a4f91db55e441113f6d3eb9b7ebb8fb806e5bb6d6bd0d \ --hash=sha256:b416252ac5588d9dfb8a30a191451adbf534e9ce5f56bb02cd193f12d8845b7f \ --hash=sha256:b5194775fec7dc3dbd6a935102bb156cd2c35efe1685b0a46c67b927c74f0cfb \ --hash=sha256:cacdef0348a08e475a721967f48206a2254a1b26ee7637638d9e081761a5ba86 \ --hash=sha256:cd1e68776262dd44dedd7381b1a0ad09d9930ffb405f737d64f505eb7f77d6c7 \ --hash=sha256:cdcda1156dcc41e042d1e899ba1f5c2e9f3cd7625b3d6ebfa619806a4c1aadda \ --hash=sha256:cf8dae9cc0412cb86c8de5a8f3be395c5119a370f3ce2e69c8b7d46bb9872c8d \ --hash=sha256:d2497769895bb03efe3187fb1888fc20e98a5f18b3d14b606167dacda5789434 \ --hash=sha256:e3b77eaefc74d7eb861d3ffbdf91b50a1bb1639514ebe764c47773b833fa2d91 \ --hash=sha256:e48cee31bc5f5a31fb2f3b573764bd563aaa5472342860edcc7039525b53e46a \ --hash=sha256:e4cbb2100ee46d024c45920d16e888ee5d3cf47c66e316210bc236d5bebc42b3 \ --hash=sha256:f28f8b2db7b86c77916829d64ab21ff49a9d8289ea1564a2b2a3a8ed9ffcccd3 \ --hash=sha256:f3023e14805c61bc439fb40ca545ac3d5740ce66120a678a3c6c2c55b70343d1 \ --hash=sha256:fdf348ae69c6ff484402cfdb14e18c1b0054ac2420079d575c53a60b9b2853ae # via # -r requirements.in # grpcio-health-checking # grpcio-reflection # grpcio-tools grpcio-health-checking==1.63.0 \ --hash=sha256:43b90ad740ae6c5655b95fe2a054d1cc05b77a1c50363540963b3b750356ab50 \ --hash=sha256:ebd4ea09aeb6ef314da86b784d2423705f378cbbfff7be7091bce29602614a54 # via -r requirements.in grpcio-reflection==1.63.0 \ --hash=sha256:280854ce7ca4e998df125c877c45cccb77c6acd55cd87c286ca0c6ef60eb95d5 \ --hash=sha256:cb53c67f1c917a66e3864e266c1bea8d77b95e10b9a21a4bb2a60c2f4d8b2b4d # via -r requirements.in grpcio-tools==1.63.0 \ --hash=sha256:0ca6d5623dadce66fabbd8b04d0572e35fd63b31f1ae7ea1555d662864852d33 \ --hash=sha256:0f8ce3fc598886a5370f28c86f94d06ddb0d3a251101a5bb8ed9576d9f86a519 \ --hash=sha256:1ab17460a2dfd3433af3120598bc18e705e3092d4d8396d3c06fe93deab19bbb \ --hash=sha256:1b88be61eaa41eb4deb6b91a1e21d2a789d8567f0a973694fa27c09196f39a9a \ --hash=sha256:2474cffbc8f29404f0e3a2109c0a0423211ba93fe048b144e734f601ff391fc7 \ --hash=sha256:27684446c81bffcd4f20f13bf672ac7cbeaefbd270b3d867cdb58132e4b866bc \ --hash=sha256:2924747142ebcbbd62acf65936fbc9694afbdfc2c6ae721461015370e27b8d6f \ --hash=sha256:32247ac2d575a633aea2536840fd232d56f309bd940081d772081bd71e0626c6 \ --hash=sha256:376136b9bbd16304a2e550ea0bb2b3340b720a0f623856124987845ef071d479 \ --hash=sha256:3ef50fa15689f46a2c903f1c9687aa40687d67dcb0469903fff37a63e55f11cd \ --hash=sha256:3f138c822090e7c87ef6a5dce0a6c4fdf68a9472e6a936b70ac7be2371184abe \ --hash=sha256:409613bb694308a1945256d1d05c3ef3497f9fbf7fd68bd8bed86d80d97df334 \ --hash=sha256:4374c8beefec84f682c799b8df5ac4b217c09de6d69038ce16fc12dcd862fff8 \ --hash=sha256:49404876ec70bdae431eac5b1591c32c0bba4047dfd96dc6add03dbcdad5a5fc \ --hash=sha256:49435413548e019921e125b178f3fd30543aa348c70775669b5ed80f0b39b393 \ --hash=sha256:49af114fed0075025fe243cb3c8405c7a99f0b87f4eb7ccdaaf33ce1f55d8318 \ --hash=sha256:517ed2b405793e55c527f332296ae92a3e17fdd83772f1569709f2c228acaf54 \ --hash=sha256:54136ac94eabc45b1b72d5ca379e5a2753f21a654f562838c5a9b706482bc1f0 \ --hash=sha256:632f78d8730d39363fc5afaf7cb5cf2f56b4e346ca11f550af74cff85e702f79 \ --hash=sha256:63a975d0457b2db1ee19fe99806091c71ad22f6f3664adc8f4c95943684dc0fd \ --hash=sha256:6bbf51f334452fcac422509979635f97e2c2c3e71f21480891f2ba280b4b6867 \ --hash=sha256:711d9f038c18c2f637b89af70c485018ae437dff5f7d2c631ca6a1eee7563038 \ --hash=sha256:744952a560fdb060a5f9d467d130fde6dbfee2abb07143c87e9b17aae3c19d5a \ --hash=sha256:7cbf570f7b9badd3bd27be5e057ca466d447c1047bf80c87a53d8bcb2e87bbbe \ --hash=sha256:8341846604df00cf1c0a822476d27f4c481f678601a2f0b190e3b9936f857ded \ --hash=sha256:847ca8d75090d66e787576049500eb7c230a9997146d5d433da7928baf914273 \ --hash=sha256:94b52c0dfb6026f69858a10ee3eadf15c343667647b5846cace82f61fe809c88 \ --hash=sha256:acb5cc845942dc0f020eefbe10ad8ac6fe2f96b99c035da738c5d3026d3a5324 \ --hash=sha256:b2d246eee3b2a6afe65362c22a98b0e6d805c227c2569c5616ad3bec619621dd \ --hash=sha256:b5d74a30409eda2a0cdaa700da23fe3cad5d7ac47ac2d52644abe13a84047aa0 \ --hash=sha256:b61682c06f0bcf2c576537819c42d5fb8eec1a0a9c05c905005200a57ff54c1f \ --hash=sha256:b87750347cb024bb74d5139da01ffba9f099ad2e43ba45893dc627ec920d57ab \ --hash=sha256:bc0e6af05f66b36186ad3467d46ecc0f51dc9fa366005e095f4aa7739c7bfcba \ --hash=sha256:c305274aa111412f5b8858242853e56c16ebcedc25d6a49ad615fd1b3ecd5971 \ --hash=sha256:c63a0f37b6b64dc31b2f1a0e5f889ae8b6bb7b7b20fe2406c1285321a7c54fdd \ --hash=sha256:c759306c04e3d0b3da3bd576e3de8bcbccc31a243a85ad256fd46d3a3ed93402 \ --hash=sha256:cb9a0f61cabff426eaf5c0a506de599c9f006b31947ba1159254cc291c1dbdd1 \ --hash=sha256:d58a5aacee102858e49b1cc89b1ba1a020bb04f001df057e2b03fa11e6c636d1 \ --hash=sha256:d7142b0162834d3a67df532744a733b0757b11056373bd489a70dc07a3f65829 \ --hash=sha256:df4dc9db9763594ae6ae04b42d6fcf7f163897a072f8fc946b864c9c3f0fbab1 \ --hash=sha256:e197d5de49bb024f3d0b9e1ee1a0cce9e39955e17738bfbed72b0cc506a4824c \ --hash=sha256:e68d9df9134906cbab1b225b625e11a368ab01b9ff24a4546bddec705ec7fd66 \ --hash=sha256:e952835e7b8f40204bceb2a96fc7bcb8b07ff45ca9d07266774bc429db1efead \ --hash=sha256:f2cc0b3098ff48811ca821440e03763dcabd11158a11d9ea819c58938a9ea276 \ --hash=sha256:f305a5d61613e7ea3510eab62d65d47dff5206fcbe3b2347a7c1ebc9eff23dc6 \ --hash=sha256:f74a6da9db48296c3e7e34820e96744a0ea9cd58c3fa075ed206f7bb75229324 # via -r requirements.in idna==3.7 \ --hash=sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc \ --hash=sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0 # via # -r requirements.in # requests protobuf==5.29.6 \ --hash=sha256:36ade6ff88212e91aef4e687a971a11d7d24d6948a66751abc1b3238648f5d05 \ --hash=sha256:62e8a3114992c7c647bce37dcc93647575fc52d50e48de30c6fcb28a6a291eb1 \ --hash=sha256:6b9edb641441b2da9fa8f428760fc136a49cf97a52076010cf22a2ff73438a86 \ --hash=sha256:76e07e6567f8baf827137e8d5b8204b6c7b6488bbbff1bf0a72b383f77999c18 \ --hash=sha256:7e6ad413275be172f67fdee0f43484b6de5a904cc1c3ea9804cb6fe2ff366eda \ --hash=sha256:831e2da16b6cc9d8f1654c041dd594eda43391affd3c03a91bea7f7f6da106d6 \ --hash=sha256:a8866b2cff111f0f863c1b3b9e7572dc7eaea23a7fae27f6fc613304046483e6 \ --hash=sha256:b5a169e664b4057183a34bdc424540e86eea47560f3c123a0d64de4e137f9269 \ --hash=sha256:cb4c86de9cd8a7f3a256b9744220d87b847371c6b2f10bde87768918ef33ba49 \ --hash=sha256:da9ee6a5424b6b30fd5e45c5ea663aef540ca95f9ad99d1e887e819cdf9b8723 \ --hash=sha256:e3387f44798ac1106af0233c04fb8abf543772ff241169946f698b3a9a3d3ab9 # via # -r requirements.in # grpcio-health-checking # grpcio-reflection # grpcio-tools python-gflags==3.1.2 \ --hash=sha256:40ae131e899ef68e9e14aa53ca063839c34f6a168afe622217b5b875492a1ee2 # via # -r requirements.in # glog pyzmq==27.0.1 \ --hash=sha256:05a94233fdde585eb70924a6e4929202a747eea6ed308a6171c4f1c715bbe39e \ --hash=sha256:092f4011b26d6b0201002f439bd74b38f23f3aefcb358621bdc3b230afc9b2d5 \ --hash=sha256:0ec09073ed67ae236785d543df3b322282acc0bdf6d1b748c3e81f3043b21cb5 \ --hash=sha256:0f772eea55cccce7f45d6ecdd1d5049c12a77ec22404f6b892fae687faa87bee \ --hash=sha256:0fc24bf45e4a454e55ef99d7f5c8b8712539200ce98533af25a5bfa954b6b390 \ --hash=sha256:119ce8590409702394f959c159d048002cbed2f3c0645ec9d6a88087fc70f0f1 \ --hash=sha256:1843fd0daebcf843fe6d4da53b8bdd3fc906ad3e97d25f51c3fed44436d82a49 \ --hash=sha256:19dce6c93656f9c469540350d29b128cd8ba55b80b332b431b9a1e9ff74cfd01 \ --hash=sha256:1c363c6dc66352331d5ad64bb838765c6692766334a6a02fdb05e76bd408ae18 \ --hash=sha256:1d59dad4173dc2a111f03e59315c7bd6e73da1a9d20a84a25cf08325b0582b1a \ --hash=sha256:1da8e645c655d86f0305fb4c65a0d848f461cd90ee07d21f254667287b5dbe50 \ --hash=sha256:2329f0c87f0466dce45bba32b63f47018dda5ca40a0085cc5c8558fea7d9fc55 \ --hash=sha256:27a78bdd384dbbe7b357af95f72efe8c494306b5ec0a03c31e2d53d6763e5307 \ --hash=sha256:2852f67371918705cc18b321695f75c5d653d5d8c4a9b946c1eec4dab2bd6fdf \ --hash=sha256:313a7b374e3dc64848644ca348a51004b41726f768b02e17e689f1322366a4d9 \ --hash=sha256:351bf5d8ca0788ca85327fda45843b6927593ff4c807faee368cc5aaf9f809c2 \ --hash=sha256:4401649bfa0a38f0f8777f8faba7cd7eb7b5b8ae2abc7542b830dd09ad4aed0d \ --hash=sha256:44909aa3ed2234d69fe81e1dade7be336bcfeab106e16bdaa3318dcde4262b93 \ --hash=sha256:45c3e00ce16896ace2cd770ab9057a7cf97d4613ea5f2a13f815141d8b6894b9 \ --hash=sha256:45c549204bc20e7484ffd2555f6cf02e572440ecf2f3bdd60d4404b20fddf64b \ --hash=sha256:497bd8af534ae55dc4ef67eebd1c149ff2a0b0f1e146db73c8b5a53d83c1a5f5 \ --hash=sha256:4b9d8e26fb600d0d69cc9933e20af08552e97cc868a183d38a5c0d661e40dfbb \ --hash=sha256:4bca8abc31799a6f3652d13f47e0b0e1cab76f9125f2283d085a3754f669b607 \ --hash=sha256:4c3874344fd5fa6d58bb51919708048ac4cab21099f40a227173cddb76b4c20b \ --hash=sha256:4f6886c59ba93ffde09b957d3e857e7950c8fe818bd5494d9b4287bc6d5bc7f1 \ --hash=sha256:5268a5a9177afff53dc6d70dffe63114ba2a6e7b20d9411cc3adeba09eeda403 \ --hash=sha256:544b995a6a1976fad5d7ff01409b4588f7608ccc41be72147700af91fd44875d \ --hash=sha256:56a3b1853f3954ec1f0e91085f1350cc57d18f11205e4ab6e83e4b7c414120e0 \ --hash=sha256:571f762aed89025ba8cdcbe355fea56889715ec06d0264fd8b6a3f3fa38154ed \ --hash=sha256:57bb92abdb48467b89c2d21da1ab01a07d0745e536d62afd2e30d5acbd0092eb \ --hash=sha256:58cca552567423f04d06a075f4b473e78ab5bdb906febe56bf4797633f54aa4e \ --hash=sha256:64ca3c7c614aefcdd5e358ecdd41d1237c35fe1417d01ec0160e7cdb0a380edc \ --hash=sha256:678e50ec112bdc6df5a83ac259a55a4ba97a8b314c325ab26b3b5b071151bc61 \ --hash=sha256:696900ef6bc20bef6a242973943574f96c3f97d2183c1bd3da5eea4f559631b1 \ --hash=sha256:6dcbcb34f5c9b0cefdfc71ff745459241b7d3cda5b27c7ad69d45afc0821d1e1 \ --hash=sha256:6f02f30a4a6b3efe665ab13a3dd47109d80326c8fd286311d1ba9f397dc5f247 \ --hash=sha256:70b719a130b81dd130a57ac0ff636dc2c0127c5b35ca5467d1b67057e3c7a4d2 \ --hash=sha256:72d235d6365ca73d8ce92f7425065d70f5c1e19baa458eb3f0d570e425b73a96 \ --hash=sha256:7418fb5736d0d39b3ecc6bec4ff549777988feb260f5381636d8bd321b653038 \ --hash=sha256:77fed80e30fa65708546c4119840a46691290efc231f6bfb2ac2a39b52e15811 \ --hash=sha256:7ebccf0d760bc92a4a7c751aeb2fef6626144aace76ee8f5a63abeb100cae87f \ --hash=sha256:7fb0ee35845bef1e8c4a152d766242164e138c239e3182f558ae15cb4a891f94 \ --hash=sha256:87aebf4acd7249bdff8d3df03aed4f09e67078e6762cfe0aecf8d0748ff94cde \ --hash=sha256:88dc92d9eb5ea4968123e74db146d770b0c8d48f0e2bfb1dbc6c50a8edb12d64 \ --hash=sha256:8c62297bc7aea2147b472ca5ca2b4389377ad82898c87cabab2a94aedd75e337 \ --hash=sha256:8f617f60a8b609a13099b313e7e525e67f84ef4524b6acad396d9ff153f6e4cd \ --hash=sha256:90a4da42aa322de8a3522461e3b5fe999935763b27f69a02fced40f4e3cf9682 \ --hash=sha256:95594b2ceeaa94934e3e94dd7bf5f3c3659cf1a26b1fb3edcf6e42dad7e0eaf2 \ --hash=sha256:9729190bd770314f5fbba42476abf6abe79a746eeda11d1d68fd56dd70e5c296 \ --hash=sha256:9d16fdfd7d70a6b0ca45d36eb19f7702fa77ef6256652f17594fc9ce534c9da6 \ --hash=sha256:9d7b6b90da7285642f480b48c9efd1d25302fd628237d8f6f6ee39ba6b2d2d34 \ --hash=sha256:a066ea6ad6218b4c233906adf0ae67830f451ed238419c0db609310dd781fbe7 \ --hash=sha256:a27fa11ebaccc099cac4309c799aa33919671a7660e29b3e465b7893bc64ec81 \ --hash=sha256:a4aca06ba295aa78bec9b33ec028d1ca08744c36294338c41432b7171060c808 \ --hash=sha256:af2ee67b3688b067e20fea3fe36b823a362609a1966e7e7a21883ae6da248804 \ --hash=sha256:af7ebce2a1e7caf30c0bb64a845f63a69e76a2fadbc1cac47178f7bb6e657bdd \ --hash=sha256:b007e5dcba684e888fbc90554cb12a2f4e492927c8c2761a80b7590209821743 \ --hash=sha256:b25e72e115399a4441aad322258fa8267b873850dc7c276e3f874042728c2b45 \ --hash=sha256:b978c0678cffbe8860ec9edc91200e895c29ae1ac8a7085f947f8e8864c489fb \ --hash=sha256:b99ea9d330e86ce1ff7f2456b33f1bf81c43862a5590faf4ef4ed3a63504bdab \ --hash=sha256:b9fd0fda730461f510cfd9a40fafa5355d65f5e3dbdd8d6dfa342b5b3f5d1949 \ --hash=sha256:ba068f28028849da725ff9185c24f832ccf9207a40f9b28ac46ab7c04994bd41 \ --hash=sha256:be45a895f98877271e8a0b6cf40925e0369121ce423421c20fa6d7958dc753c2 \ --hash=sha256:bee5248d5ec9223545f8cc4f368c2d571477ae828c99409125c3911511d98245 \ --hash=sha256:c512824360ea7490390566ce00bee880e19b526b312b25cc0bc30a0fe95cb67f \ --hash=sha256:c9180d1f5b4b73e28b64e63cc6c4c097690f102aa14935a62d5dd7426a4e5b5a \ --hash=sha256:c96702e1082eab62ae583d64c4e19c9b848359196697e536a0c57ae9bd165bd5 \ --hash=sha256:c9d63d66059114a6756d09169c9209ffceabacb65b9cb0f66e6fc344b20b73e6 \ --hash=sha256:ce181dd1a7c6c012d0efa8ab603c34b5ee9d86e570c03415bbb1b8772eeb381c \ --hash=sha256:d0356a21e58c3e99248930ff73cc05b1d302ff50f41a8a47371aefb04327378a \ --hash=sha256:d0b96c30be9f9387b18b18b6133c75a7b1b0065da64e150fe1feb5ebf31ece1c \ --hash=sha256:d2976b7079f09f48d59dc123293ed6282fca6ef96a270f4ea0364e4e54c8e855 \ --hash=sha256:d97b59cbd8a6c8b23524a8ce237ff9504d987dc07156258aa68ae06d2dd5f34d \ --hash=sha256:da81512b83032ed6cdf85ca62e020b4c23dda87f1b6c26b932131222ccfdbd27 \ --hash=sha256:df2c55c958d3766bdb3e9d858b911288acec09a9aab15883f384fc7180df5bed \ --hash=sha256:dfb2bb5e0f7198eaacfb6796fb0330afd28f36d985a770745fba554a5903595a \ --hash=sha256:e4f22d67756518d71901edf73b38dc0eb4765cce22c8fe122cc81748d425262b \ --hash=sha256:e648dca28178fc879c814cf285048dd22fd1f03e1104101106505ec0eea50a4d \ --hash=sha256:e971d8680003d0af6020713e52f92109b46fedb463916e988814e04c8133578a \ --hash=sha256:ee16906c8025fa464bea1e48128c048d02359fb40bebe5333103228528506530 \ --hash=sha256:f293a1419266e3bf3557d1f8778f9e1ffe7e6b2c8df5c9dca191caf60831eb74 \ --hash=sha256:f379f11e138dfd56c3f24a04164f871a08281194dd9ddf656a278d7d080c8ad0 \ --hash=sha256:f44e7ea288d022d4bf93b9e79dafcb4a7aea45a3cbeae2116792904931cefccf \ --hash=sha256:f5b6133c8d313bde8bd0d123c169d22525300ff164c2189f849de495e1344577 \ --hash=sha256:f65741cc06630652e82aa68ddef4986a3ab9073dd46d59f94ce5f005fa72037c \ --hash=sha256:f8c3b74f1cd577a5a9253eae7ed363f88cbb345a990ca3027e9038301d47c7f4 \ --hash=sha256:f96a63aecec22d3f7fdea3c6c98df9e42973f5856bb6812c3d8d78c262fee808 \ --hash=sha256:f98f6b7787bd2beb1f0dde03f23a0621a0c978edf673b7d8f5e7bc039cbe1b60 \ --hash=sha256:fde26267416c8478c95432c81489b53f57b0b5d24cd5c8bfaebf5bbaac4dc90c \ --hash=sha256:fe632fa4501154d58dfbe1764a0495734d55f84eaf1feda4549a1f1ca76659e9 \ --hash=sha256:ff3f8757570e45da7a5bedaa140489846510014f7a9d5ee9301c61f3f1b8a686 \ --hash=sha256:ffe6b809a97ac6dea524b3b837d5b28743d8c2f121141056d168ff0ba8f614ef # via -r requirements.in requests==2.32.4 \ --hash=sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c \ --hash=sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422 # via # -r requirements.in # requests-mock requests-mock==1.12.1 \ --hash=sha256:b1e37054004cdd5e56c84454cc7df12b25f90f382159087f4b6915aaeef39563 \ --hash=sha256:e9e12e333b525156e82a3c852f22016b9158220d2f47454de9cae8a77d371401 # via -r requirements.in ruamel-yaml==0.18.6 \ --hash=sha256:57b53ba33def16c4f3d807c0ccbc00f8a6081827e81ba2491691b76882d0c636 \ --hash=sha256:8b27e6a217e786c6fbe5634d8f3f11bc63e0f80f6a5890f28863d9c45aac311b # via -r requirements.in ruamel-yaml-clib==0.2.8 \ --hash=sha256:024cfe1fc7c7f4e1aff4a81e718109e13409767e4f871443cbff3dba3578203d \ --hash=sha256:03d1162b6d1df1caa3a4bd27aa51ce17c9afc2046c31b0ad60a0a96ec22f8001 \ --hash=sha256:07238db9cbdf8fc1e9de2489a4f68474e70dffcb32232db7c08fa61ca0c7c462 \ --hash=sha256:09b055c05697b38ecacb7ac50bdab2240bfca1a0c4872b0fd309bb07dc9aa3a9 \ --hash=sha256:1707814f0d9791df063f8c19bb51b0d1278b8e9a2353abbb676c2f685dee6afe \ --hash=sha256:1758ce7d8e1a29d23de54a16ae867abd370f01b5a69e1a3ba75223eaa3ca1a1b \ --hash=sha256:184565012b60405d93838167f425713180b949e9d8dd0bbc7b49f074407c5a8b \ --hash=sha256:1b617618914cb00bf5c34d4357c37aa15183fa229b24767259657746c9077615 \ --hash=sha256:1dc67314e7e1086c9fdf2680b7b6c2be1c0d8e3a8279f2e993ca2a7545fecf62 \ --hash=sha256:25ac8c08322002b06fa1d49d1646181f0b2c72f5cbc15a85e80b4c30a544bb15 \ --hash=sha256:25c515e350e5b739842fc3228d662413ef28f295791af5e5110b543cf0b57d9b \ --hash=sha256:305889baa4043a09e5b76f8e2a51d4ffba44259f6b4c72dec8ca56207d9c6fe1 \ --hash=sha256:3213ece08ea033eb159ac52ae052a4899b56ecc124bb80020d9bbceeb50258e9 \ --hash=sha256:3f215c5daf6a9d7bbed4a0a4f760f3113b10e82ff4c5c44bec20a68c8014f675 \ --hash=sha256:46d378daaac94f454b3a0e3d8d78cafd78a026b1d71443f4966c696b48a6d899 \ --hash=sha256:4ecbf9c3e19f9562c7fdd462e8d18dd902a47ca046a2e64dba80699f0b6c09b7 \ --hash=sha256:53a300ed9cea38cf5a2a9b069058137c2ca1ce658a874b79baceb8f892f915a7 \ --hash=sha256:56f4252222c067b4ce51ae12cbac231bce32aee1d33fbfc9d17e5b8d6966c312 \ --hash=sha256:5c365d91c88390c8d0a8545df0b5857172824b1c604e867161e6b3d59a827eaa \ --hash=sha256:700e4ebb569e59e16a976857c8798aee258dceac7c7d6b50cab63e080058df91 \ --hash=sha256:75e1ed13e1f9de23c5607fe6bd1aeaae21e523b32d83bb33918245361e9cc51b \ --hash=sha256:77159f5d5b5c14f7c34073862a6b7d34944075d9f93e681638f6d753606c6ce6 \ --hash=sha256:7f67a1ee819dc4562d444bbafb135832b0b909f81cc90f7aa00260968c9ca1b3 \ --hash=sha256:840f0c7f194986a63d2c2465ca63af8ccbbc90ab1c6001b1978f05119b5e7334 \ --hash=sha256:84b554931e932c46f94ab306913ad7e11bba988104c5cff26d90d03f68258cd5 \ --hash=sha256:87ea5ff66d8064301a154b3933ae406b0863402a799b16e4a1d24d9fbbcbe0d3 \ --hash=sha256:955eae71ac26c1ab35924203fda6220f84dce57d6d7884f189743e2abe3a9fbe \ --hash=sha256:a1a45e0bb052edf6a1d3a93baef85319733a888363938e1fc9924cb00c8df24c \ --hash=sha256:a5aa27bad2bb83670b71683aae140a1f52b0857a2deff56ad3f6c13a017a26ed \ --hash=sha256:a6a9ffd280b71ad062eae53ac1659ad86a17f59a0fdc7699fd9be40525153337 \ --hash=sha256:a75879bacf2c987c003368cf14bed0ffe99e8e85acfa6c0bfffc21a090f16880 \ --hash=sha256:aa2267c6a303eb483de8d02db2871afb5c5fc15618d894300b88958f729ad74f \ --hash=sha256:aab7fd643f71d7946f2ee58cc88c9b7bfc97debd71dcc93e03e2d174628e7e2d \ --hash=sha256:b16420e621d26fdfa949a8b4b47ade8810c56002f5389970db4ddda51dbff248 \ --hash=sha256:b42169467c42b692c19cf539c38d4602069d8c1505e97b86387fcf7afb766e1d \ --hash=sha256:bba64af9fa9cebe325a62fa398760f5c7206b215201b0ec825005f1b18b9bccf \ --hash=sha256:beb2e0404003de9a4cab9753a8805a8fe9320ee6673136ed7f04255fe60bb512 \ --hash=sha256:bef08cd86169d9eafb3ccb0a39edb11d8e25f3dae2b28f5c52fd997521133069 \ --hash=sha256:c2a72e9109ea74e511e29032f3b670835f8a59bbdc9ce692c5b4ed91ccf1eedb \ --hash=sha256:c58ecd827313af6864893e7af0a3bb85fd529f862b6adbefe14643947cfe2942 \ --hash=sha256:c69212f63169ec1cfc9bb44723bf2917cbbd8f6191a00ef3410f5a7fe300722d \ --hash=sha256:cabddb8d8ead485e255fe80429f833172b4cadf99274db39abc080e068cbcc31 \ --hash=sha256:d176b57452ab5b7028ac47e7b3cf644bcfdc8cacfecf7e71759f7f51a59e5c92 \ --hash=sha256:da09ad1c359a728e112d60116f626cc9f29730ff3e0e7db72b9a2dbc2e4beed5 \ --hash=sha256:e2b4c44b60eadec492926a7270abb100ef9f72798e18743939bdbf037aab8c28 \ --hash=sha256:e79e5db08739731b0ce4850bed599235d601701d5694c36570a99a0c5ca41a9d \ --hash=sha256:ebc06178e8821efc9692ea7544aa5644217358490145629914d8020042c24aa1 \ --hash=sha256:edaef1c1200c4b4cb914583150dcaa3bc30e592e907c01117c08b13a07255ec2 \ --hash=sha256:f481f16baec5290e45aebdc2a5168ebc6d35189ae6fea7a58787613a25f6e875 \ --hash=sha256:fff3573c2db359f091e1589c3d7c5fc2f86f5bdb6f24252c2d8e539d4e45f412 # via # -r requirements.in # ruamel-yaml six==1.16.0 \ --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \ --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 # via # -r requirements.in # glog urllib3==2.6.3 \ --hash=sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed \ --hash=sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4 # via # -r requirements.in # requests # The following packages are considered to be unsafe in a requirements file: setuptools==80.9.0 \ --hash=sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922 \ --hash=sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c # via grpcio-tools ================================================ FILE: plugin_server/py/tsunami_plugin.py ================================================ # Copyright 2022 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Interface that all Python TsunamiPlugins will need to implement to run detection.""" import abc import detection_pb2 import network_service_pb2 import plugin_representation_pb2 import reconnaissance_pb2 import vulnerability_pb2 TargetInfo = reconnaissance_pb2.TargetInfo NetworkService = network_service_pb2.NetworkService DetectionReportList = detection_pb2.DetectionReportList PluginDefinition = plugin_representation_pb2.PluginDefinition class TsunamiPlugin(metaclass=abc.ABCMeta): @abc.abstractmethod def GetPluginDefinition(self) -> PluginDefinition: pass @classmethod def __subclasshook__(cls, subclass: abc.ABCMeta) -> bool: return hasattr(subclass, 'GetPluginDefinition') and callable( subclass.GetPluginDefinition ) class VulnDetector(TsunamiPlugin): """A TsunamiPlugin that detects potential vulnerabilities on the target. Usually a vulnerability detector takes the information about an exposed network service, detects whether the service is vulnerable to a specific vulnerability, and reports the detection results. """ @classmethod def __init_subclass__(cls, **kwargs): super().__init_subclass__(**kwargs) @abc.abstractmethod def GetAdvisories(self) -> list[vulnerability_pb2.Vulnerability]: """Returns the list of vulnerabilities detected by this plugin.""" @abc.abstractmethod def Detect( self, target: TargetInfo, matched_services: list[NetworkService] ) -> DetectionReportList: """Run detection logic for the target. Args: target: Information about the scanning target itself. matched_services: A list of network services whose vulnerabilities could be detected by this plugin. Returns: A DetectionReportList for all the vulnerabilities of the scanning target. """ ================================================ FILE: proto/build.gradle ================================================ plugins { id 'com.google.protobuf' } description = 'Tsunami: Protobuf Data' sourceSets { main { proto { srcDir "${projectDir}" exclude "build/**" } } } protobuf { generatedFilesBaseDir = "${projectDir}/build/generated" protoc { artifact = "com.google.protobuf:protoc:3.25.2" } plugins { grpc { artifact = "io.grpc:protoc-gen-grpc-java:1.60.0" } } generateProtoTasks { all()*.plugins { grpc {} } all().configureEach { task -> { dependsOn("processResources") dependsOn("extractTestProto") dependsOn("sourcesJar") } } } } idea { module { sourceDirs += file("${projectDir}/build/generated/main/java"); sourceDirs += file("${projectDir}/build/generated/main/grpc"); } } dependencies { implementation "com.google.protobuf:protobuf-java:3.25.5" implementation "io.grpc:grpc-protobuf:1.60.0" implementation "io.grpc:grpc-stub:1.60.0" implementation "javax.annotation:javax.annotation-api:1.3.2" } gradle.projectsEvaluated { tasks.withType(ProcessResources) { dependsOn("extractProto") } } ================================================ FILE: proto/detection.proto ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ // Data models for describing a vulnerability detection report. syntax = "proto3"; package tsunami.proto; import "google/protobuf/timestamp.proto"; import "network_service.proto"; import "reconnaissance.proto"; import "vulnerability.proto"; option java_multiple_files = true; option java_outer_classname = "DetectionProtos"; option java_package = "com.google.tsunami.proto"; option go_package = "github.com/google/tsunami-security-scanner/proto/go/detection_go_proto"; // Status of the vulnerability detection result. enum DetectionStatus { // Unspecified status. DETECTION_STATUS_UNSPECIFIED = 0; // Target is not vulnerable. SAFE = 1; // Target appears to be vulnerable (e.g. because running version is // vulnerable), but couldn't be verified. VULNERABILITY_PRESENT = 2; // Target is vulnerable and the detector successfully verified the // vulnerability. VULNERABILITY_VERIFIED = 3; } // Full report about a detected vulnerability. message DetectionReport { // Information about the scanned target. TargetInfo target_info = 1; // Information about the scanned network service. NetworkService network_service = 2; // Time when the vulnerability was detected. google.protobuf.Timestamp detection_timestamp = 3; // Status of the detection result. DetectionStatus detection_status = 4; // Full details about the detected vulnerability. Vulnerability vulnerability = 5; } message DetectionReportList { repeated DetectionReport detection_reports = 1; } ================================================ FILE: proto/go/detection_go_proto/detection.pb.go ================================================ // // Copyright 2020 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Data models for describing a vulnerability detection report. // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.11 // protoc v3.21.12 // source: detection.proto package detection_go_proto import ( network_service_go_proto "github.com/google/tsunami-security-scanner/proto/go/network_service_go_proto" reconnaissance_go_proto "github.com/google/tsunami-security-scanner/proto/go/reconnaissance_go_proto" vulnerability_go_proto "github.com/google/tsunami-security-scanner/proto/go/vulnerability_go_proto" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" timestamppb "google.golang.org/protobuf/types/known/timestamppb" reflect "reflect" unsafe "unsafe" ) const ( // Verify that this generated code is sufficiently up-to-date. _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) // Verify that runtime/protoimpl is sufficiently up-to-date. _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) // Status of the vulnerability detection result. type DetectionStatus int32 const ( // Unspecified status. DetectionStatus_DETECTION_STATUS_UNSPECIFIED DetectionStatus = 0 // Target is not vulnerable. DetectionStatus_SAFE DetectionStatus = 1 // Target appears to be vulnerable (e.g. because running version is // vulnerable), but couldn't be verified. DetectionStatus_VULNERABILITY_PRESENT DetectionStatus = 2 // Target is vulnerable and the detector successfully verified the // vulnerability. DetectionStatus_VULNERABILITY_VERIFIED DetectionStatus = 3 ) // Enum value maps for DetectionStatus. var ( DetectionStatus_name = map[int32]string{ 0: "DETECTION_STATUS_UNSPECIFIED", 1: "SAFE", 2: "VULNERABILITY_PRESENT", 3: "VULNERABILITY_VERIFIED", } DetectionStatus_value = map[string]int32{ "DETECTION_STATUS_UNSPECIFIED": 0, "SAFE": 1, "VULNERABILITY_PRESENT": 2, "VULNERABILITY_VERIFIED": 3, } ) func (x DetectionStatus) Enum() *DetectionStatus { p := new(DetectionStatus) *p = x return p } func (x DetectionStatus) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (DetectionStatus) Descriptor() protoreflect.EnumDescriptor { return file_detection_proto_enumTypes[0].Descriptor() } func (DetectionStatus) Type() protoreflect.EnumType { return &file_detection_proto_enumTypes[0] } func (x DetectionStatus) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } // Full report about a detected vulnerability. type DetectionReport struct { state protoimpl.MessageState `protogen:"opaque.v1"` xxx_hidden_TargetInfo *reconnaissance_go_proto.TargetInfo `protobuf:"bytes,1,opt,name=target_info,json=targetInfo,proto3"` xxx_hidden_NetworkService *network_service_go_proto.NetworkService `protobuf:"bytes,2,opt,name=network_service,json=networkService,proto3"` xxx_hidden_DetectionTimestamp *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=detection_timestamp,json=detectionTimestamp,proto3"` xxx_hidden_DetectionStatus DetectionStatus `protobuf:"varint,4,opt,name=detection_status,json=detectionStatus,proto3,enum=tsunami.proto.DetectionStatus"` xxx_hidden_Vulnerability *vulnerability_go_proto.Vulnerability `protobuf:"bytes,5,opt,name=vulnerability,proto3"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *DetectionReport) Reset() { *x = DetectionReport{} mi := &file_detection_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *DetectionReport) String() string { return protoimpl.X.MessageStringOf(x) } func (*DetectionReport) ProtoMessage() {} func (x *DetectionReport) ProtoReflect() protoreflect.Message { mi := &file_detection_proto_msgTypes[0] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } func (x *DetectionReport) GetTargetInfo() *reconnaissance_go_proto.TargetInfo { if x != nil { return x.xxx_hidden_TargetInfo } return nil } func (x *DetectionReport) GetNetworkService() *network_service_go_proto.NetworkService { if x != nil { return x.xxx_hidden_NetworkService } return nil } func (x *DetectionReport) GetDetectionTimestamp() *timestamppb.Timestamp { if x != nil { return x.xxx_hidden_DetectionTimestamp } return nil } func (x *DetectionReport) GetDetectionStatus() DetectionStatus { if x != nil { return x.xxx_hidden_DetectionStatus } return DetectionStatus_DETECTION_STATUS_UNSPECIFIED } func (x *DetectionReport) GetVulnerability() *vulnerability_go_proto.Vulnerability { if x != nil { return x.xxx_hidden_Vulnerability } return nil } func (x *DetectionReport) SetTargetInfo(v *reconnaissance_go_proto.TargetInfo) { x.xxx_hidden_TargetInfo = v } func (x *DetectionReport) SetNetworkService(v *network_service_go_proto.NetworkService) { x.xxx_hidden_NetworkService = v } func (x *DetectionReport) SetDetectionTimestamp(v *timestamppb.Timestamp) { x.xxx_hidden_DetectionTimestamp = v } func (x *DetectionReport) SetDetectionStatus(v DetectionStatus) { x.xxx_hidden_DetectionStatus = v } func (x *DetectionReport) SetVulnerability(v *vulnerability_go_proto.Vulnerability) { x.xxx_hidden_Vulnerability = v } func (x *DetectionReport) HasTargetInfo() bool { if x == nil { return false } return x.xxx_hidden_TargetInfo != nil } func (x *DetectionReport) HasNetworkService() bool { if x == nil { return false } return x.xxx_hidden_NetworkService != nil } func (x *DetectionReport) HasDetectionTimestamp() bool { if x == nil { return false } return x.xxx_hidden_DetectionTimestamp != nil } func (x *DetectionReport) HasVulnerability() bool { if x == nil { return false } return x.xxx_hidden_Vulnerability != nil } func (x *DetectionReport) ClearTargetInfo() { x.xxx_hidden_TargetInfo = nil } func (x *DetectionReport) ClearNetworkService() { x.xxx_hidden_NetworkService = nil } func (x *DetectionReport) ClearDetectionTimestamp() { x.xxx_hidden_DetectionTimestamp = nil } func (x *DetectionReport) ClearVulnerability() { x.xxx_hidden_Vulnerability = nil } type DetectionReport_builder struct { _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. // Information about the scanned target. TargetInfo *reconnaissance_go_proto.TargetInfo // Information about the scanned network service. NetworkService *network_service_go_proto.NetworkService // Time when the vulnerability was detected. DetectionTimestamp *timestamppb.Timestamp // Status of the detection result. DetectionStatus DetectionStatus // Full details about the detected vulnerability. Vulnerability *vulnerability_go_proto.Vulnerability } func (b0 DetectionReport_builder) Build() *DetectionReport { m0 := &DetectionReport{} b, x := &b0, m0 _, _ = b, x x.xxx_hidden_TargetInfo = b.TargetInfo x.xxx_hidden_NetworkService = b.NetworkService x.xxx_hidden_DetectionTimestamp = b.DetectionTimestamp x.xxx_hidden_DetectionStatus = b.DetectionStatus x.xxx_hidden_Vulnerability = b.Vulnerability return m0 } type DetectionReportList struct { state protoimpl.MessageState `protogen:"opaque.v1"` xxx_hidden_DetectionReports *[]*DetectionReport `protobuf:"bytes,1,rep,name=detection_reports,json=detectionReports,proto3"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *DetectionReportList) Reset() { *x = DetectionReportList{} mi := &file_detection_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *DetectionReportList) String() string { return protoimpl.X.MessageStringOf(x) } func (*DetectionReportList) ProtoMessage() {} func (x *DetectionReportList) ProtoReflect() protoreflect.Message { mi := &file_detection_proto_msgTypes[1] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } func (x *DetectionReportList) GetDetectionReports() []*DetectionReport { if x != nil { if x.xxx_hidden_DetectionReports != nil { return *x.xxx_hidden_DetectionReports } } return nil } func (x *DetectionReportList) SetDetectionReports(v []*DetectionReport) { x.xxx_hidden_DetectionReports = &v } type DetectionReportList_builder struct { _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. DetectionReports []*DetectionReport } func (b0 DetectionReportList_builder) Build() *DetectionReportList { m0 := &DetectionReportList{} b, x := &b0, m0 _, _ = b, x x.xxx_hidden_DetectionReports = &b.DetectionReports return m0 } var File_detection_proto protoreflect.FileDescriptor const file_detection_proto_rawDesc = "" + "\n" + "\x0fdetection.proto\x12\rtsunami.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x15network_service.proto\x1a\x14reconnaissance.proto\x1a\x13vulnerability.proto\"\xf1\x02\n" + "\x0fDetectionReport\x12:\n" + "\vtarget_info\x18\x01 \x01(\v2\x19.tsunami.proto.TargetInfoR\n" + "targetInfo\x12F\n" + "\x0fnetwork_service\x18\x02 \x01(\v2\x1d.tsunami.proto.NetworkServiceR\x0enetworkService\x12K\n" + "\x13detection_timestamp\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\x12detectionTimestamp\x12I\n" + "\x10detection_status\x18\x04 \x01(\x0e2\x1e.tsunami.proto.DetectionStatusR\x0fdetectionStatus\x12B\n" + "\rvulnerability\x18\x05 \x01(\v2\x1c.tsunami.proto.VulnerabilityR\rvulnerability\"b\n" + "\x13DetectionReportList\x12K\n" + "\x11detection_reports\x18\x01 \x03(\v2\x1e.tsunami.proto.DetectionReportR\x10detectionReports*t\n" + "\x0fDetectionStatus\x12 \n" + "\x1cDETECTION_STATUS_UNSPECIFIED\x10\x00\x12\b\n" + "\x04SAFE\x10\x01\x12\x19\n" + "\x15VULNERABILITY_PRESENT\x10\x02\x12\x1a\n" + "\x16VULNERABILITY_VERIFIED\x10\x03Bu\n" + "\x18com.google.tsunami.protoB\x0fDetectionProtosP\x01ZFgithub.com/google/tsunami-security-scanner/proto/go/detection_go_protob\x06proto3" var file_detection_proto_enumTypes = make([]protoimpl.EnumInfo, 1) var file_detection_proto_msgTypes = make([]protoimpl.MessageInfo, 2) var file_detection_proto_goTypes = []any{ (DetectionStatus)(0), // 0: tsunami.proto.DetectionStatus (*DetectionReport)(nil), // 1: tsunami.proto.DetectionReport (*DetectionReportList)(nil), // 2: tsunami.proto.DetectionReportList (*reconnaissance_go_proto.TargetInfo)(nil), // 3: tsunami.proto.TargetInfo (*network_service_go_proto.NetworkService)(nil), // 4: tsunami.proto.NetworkService (*timestamppb.Timestamp)(nil), // 5: google.protobuf.Timestamp (*vulnerability_go_proto.Vulnerability)(nil), // 6: tsunami.proto.Vulnerability } var file_detection_proto_depIdxs = []int32{ 3, // 0: tsunami.proto.DetectionReport.target_info:type_name -> tsunami.proto.TargetInfo 4, // 1: tsunami.proto.DetectionReport.network_service:type_name -> tsunami.proto.NetworkService 5, // 2: tsunami.proto.DetectionReport.detection_timestamp:type_name -> google.protobuf.Timestamp 0, // 3: tsunami.proto.DetectionReport.detection_status:type_name -> tsunami.proto.DetectionStatus 6, // 4: tsunami.proto.DetectionReport.vulnerability:type_name -> tsunami.proto.Vulnerability 1, // 5: tsunami.proto.DetectionReportList.detection_reports:type_name -> tsunami.proto.DetectionReport 6, // [6:6] is the sub-list for method output_type 6, // [6:6] is the sub-list for method input_type 6, // [6:6] is the sub-list for extension type_name 6, // [6:6] is the sub-list for extension extendee 0, // [0:6] is the sub-list for field type_name } func init() { file_detection_proto_init() } func file_detection_proto_init() { if File_detection_proto != nil { return } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_detection_proto_rawDesc), len(file_detection_proto_rawDesc)), NumEnums: 1, NumMessages: 2, NumExtensions: 0, NumServices: 0, }, GoTypes: file_detection_proto_goTypes, DependencyIndexes: file_detection_proto_depIdxs, EnumInfos: file_detection_proto_enumTypes, MessageInfos: file_detection_proto_msgTypes, }.Build() File_detection_proto = out.File file_detection_proto_goTypes = nil file_detection_proto_depIdxs = nil } ================================================ FILE: proto/go/network_go_proto/network.pb.go ================================================ // // Copyright 2019 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Data models for describing network related information. // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.11 // protoc v3.21.12 // source: network.proto package network_go_proto import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" unsafe "unsafe" ) const ( // Verify that this generated code is sufficiently up-to-date. _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) // Verify that runtime/protoimpl is sufficiently up-to-date. _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) // The address family of an IP address. type AddressFamily int32 const ( AddressFamily_ADDRESS_FAMILY_UNSPECIFIED AddressFamily = 0 AddressFamily_IPV4 AddressFamily = 4 AddressFamily_IPV6 AddressFamily = 6 ) // Enum value maps for AddressFamily. var ( AddressFamily_name = map[int32]string{ 0: "ADDRESS_FAMILY_UNSPECIFIED", 4: "IPV4", 6: "IPV6", } AddressFamily_value = map[string]int32{ "ADDRESS_FAMILY_UNSPECIFIED": 0, "IPV4": 4, "IPV6": 6, } ) func (x AddressFamily) Enum() *AddressFamily { p := new(AddressFamily) *p = x return p } func (x AddressFamily) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (AddressFamily) Descriptor() protoreflect.EnumDescriptor { return file_network_proto_enumTypes[0].Descriptor() } func (AddressFamily) Type() protoreflect.EnumType { return &file_network_proto_enumTypes[0] } func (x AddressFamily) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } // The transport layer protocols. type TransportProtocol int32 const ( TransportProtocol_TRANSPORT_PROTOCOL_UNSPECIFIED TransportProtocol = 0 TransportProtocol_TCP TransportProtocol = 1 TransportProtocol_UDP TransportProtocol = 2 TransportProtocol_SCTP TransportProtocol = 3 ) // Enum value maps for TransportProtocol. var ( TransportProtocol_name = map[int32]string{ 0: "TRANSPORT_PROTOCOL_UNSPECIFIED", 1: "TCP", 2: "UDP", 3: "SCTP", } TransportProtocol_value = map[string]int32{ "TRANSPORT_PROTOCOL_UNSPECIFIED": 0, "TCP": 1, "UDP": 2, "SCTP": 3, } ) func (x TransportProtocol) Enum() *TransportProtocol { p := new(TransportProtocol) *p = x return p } func (x TransportProtocol) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (TransportProtocol) Descriptor() protoreflect.EnumDescriptor { return file_network_proto_enumTypes[1].Descriptor() } func (TransportProtocol) Type() protoreflect.EnumType { return &file_network_proto_enumTypes[1] } func (x TransportProtocol) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } type NetworkEndpoint_Type int32 const ( NetworkEndpoint_TYPE_UNSPECIFIED NetworkEndpoint_Type = 0 // The network endpoint is represented by an IP address. NetworkEndpoint_IP NetworkEndpoint_Type = 1 // The network endpoint is represented by IP address and port pair. NetworkEndpoint_IP_PORT NetworkEndpoint_Type = 2 // The network endpoint is represented by a hostname. NetworkEndpoint_HOSTNAME NetworkEndpoint_Type = 3 // The network endpoint is represented by a hostname and port pair. NetworkEndpoint_HOSTNAME_PORT NetworkEndpoint_Type = 4 // The network endpoint is represented by an IP address and hostname. NetworkEndpoint_IP_HOSTNAME NetworkEndpoint_Type = 5 // The network endpoint is represented by an IP address, hostname and port. NetworkEndpoint_IP_HOSTNAME_PORT NetworkEndpoint_Type = 6 ) // Enum value maps for NetworkEndpoint_Type. var ( NetworkEndpoint_Type_name = map[int32]string{ 0: "TYPE_UNSPECIFIED", 1: "IP", 2: "IP_PORT", 3: "HOSTNAME", 4: "HOSTNAME_PORT", 5: "IP_HOSTNAME", 6: "IP_HOSTNAME_PORT", } NetworkEndpoint_Type_value = map[string]int32{ "TYPE_UNSPECIFIED": 0, "IP": 1, "IP_PORT": 2, "HOSTNAME": 3, "HOSTNAME_PORT": 4, "IP_HOSTNAME": 5, "IP_HOSTNAME_PORT": 6, } ) func (x NetworkEndpoint_Type) Enum() *NetworkEndpoint_Type { p := new(NetworkEndpoint_Type) *p = x return p } func (x NetworkEndpoint_Type) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (NetworkEndpoint_Type) Descriptor() protoreflect.EnumDescriptor { return file_network_proto_enumTypes[2].Descriptor() } func (NetworkEndpoint_Type) Type() protoreflect.EnumType { return &file_network_proto_enumTypes[2] } func (x NetworkEndpoint_Type) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } // The IP address of a networking device. type IpAddress struct { state protoimpl.MessageState `protogen:"opaque.v1"` xxx_hidden_AddressFamily AddressFamily `protobuf:"varint,1,opt,name=address_family,json=addressFamily,proto3,enum=tsunami.proto.AddressFamily"` xxx_hidden_Address string `protobuf:"bytes,2,opt,name=address,proto3"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *IpAddress) Reset() { *x = IpAddress{} mi := &file_network_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *IpAddress) String() string { return protoimpl.X.MessageStringOf(x) } func (*IpAddress) ProtoMessage() {} func (x *IpAddress) ProtoReflect() protoreflect.Message { mi := &file_network_proto_msgTypes[0] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } func (x *IpAddress) GetAddressFamily() AddressFamily { if x != nil { return x.xxx_hidden_AddressFamily } return AddressFamily_ADDRESS_FAMILY_UNSPECIFIED } func (x *IpAddress) GetAddress() string { if x != nil { return x.xxx_hidden_Address } return "" } func (x *IpAddress) SetAddressFamily(v AddressFamily) { x.xxx_hidden_AddressFamily = v } func (x *IpAddress) SetAddress(v string) { x.xxx_hidden_Address = v } type IpAddress_builder struct { _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. // The family of the IP address. AddressFamily AddressFamily // A human-readable representation of the IP address, e.g. 127.0.0.1 for IPV4 // and 2001:db8:0:1234:0:567:8:1 for IPV6. Address string } func (b0 IpAddress_builder) Build() *IpAddress { m0 := &IpAddress{} b, x := &b0, m0 _, _ = b, x x.xxx_hidden_AddressFamily = b.AddressFamily x.xxx_hidden_Address = b.Address return m0 } // The port that a network service listens to. type Port struct { state protoimpl.MessageState `protogen:"opaque.v1"` xxx_hidden_PortNumber uint32 `protobuf:"varint,1,opt,name=port_number,json=portNumber,proto3"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *Port) Reset() { *x = Port{} mi := &file_network_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *Port) String() string { return protoimpl.X.MessageStringOf(x) } func (*Port) ProtoMessage() {} func (x *Port) ProtoReflect() protoreflect.Message { mi := &file_network_proto_msgTypes[1] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } func (x *Port) GetPortNumber() uint32 { if x != nil { return x.xxx_hidden_PortNumber } return 0 } func (x *Port) SetPortNumber(v uint32) { x.xxx_hidden_PortNumber = v } type Port_builder struct { _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. PortNumber uint32 } func (b0 Port_builder) Build() *Port { m0 := &Port{} b, x := &b0, m0 _, _ = b, x x.xxx_hidden_PortNumber = b.PortNumber return m0 } // The hostname of a networking device. type Hostname struct { state protoimpl.MessageState `protogen:"opaque.v1"` xxx_hidden_Name string `protobuf:"bytes,1,opt,name=name,proto3"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *Hostname) Reset() { *x = Hostname{} mi := &file_network_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *Hostname) String() string { return protoimpl.X.MessageStringOf(x) } func (*Hostname) ProtoMessage() {} func (x *Hostname) ProtoReflect() protoreflect.Message { mi := &file_network_proto_msgTypes[2] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } func (x *Hostname) GetName() string { if x != nil { return x.xxx_hidden_Name } return "" } func (x *Hostname) SetName(v string) { x.xxx_hidden_Name = v } type Hostname_builder struct { _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. Name string } func (b0 Hostname_builder) Build() *Hostname { m0 := &Hostname{} b, x := &b0, m0 _, _ = b, x x.xxx_hidden_Name = b.Name return m0 } // A classification of an endpoint for a network device. type NetworkEndpoint struct { state protoimpl.MessageState `protogen:"opaque.v1"` xxx_hidden_Type NetworkEndpoint_Type `protobuf:"varint,1,opt,name=type,proto3,enum=tsunami.proto.NetworkEndpoint_Type"` xxx_hidden_IpAddress *IpAddress `protobuf:"bytes,2,opt,name=ip_address,json=ipAddress,proto3"` xxx_hidden_Port *Port `protobuf:"bytes,3,opt,name=port,proto3"` xxx_hidden_Hostname *Hostname `protobuf:"bytes,4,opt,name=hostname,proto3"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *NetworkEndpoint) Reset() { *x = NetworkEndpoint{} mi := &file_network_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *NetworkEndpoint) String() string { return protoimpl.X.MessageStringOf(x) } func (*NetworkEndpoint) ProtoMessage() {} func (x *NetworkEndpoint) ProtoReflect() protoreflect.Message { mi := &file_network_proto_msgTypes[3] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } func (x *NetworkEndpoint) GetType() NetworkEndpoint_Type { if x != nil { return x.xxx_hidden_Type } return NetworkEndpoint_TYPE_UNSPECIFIED } func (x *NetworkEndpoint) GetIpAddress() *IpAddress { if x != nil { return x.xxx_hidden_IpAddress } return nil } func (x *NetworkEndpoint) GetPort() *Port { if x != nil { return x.xxx_hidden_Port } return nil } func (x *NetworkEndpoint) GetHostname() *Hostname { if x != nil { return x.xxx_hidden_Hostname } return nil } func (x *NetworkEndpoint) SetType(v NetworkEndpoint_Type) { x.xxx_hidden_Type = v } func (x *NetworkEndpoint) SetIpAddress(v *IpAddress) { x.xxx_hidden_IpAddress = v } func (x *NetworkEndpoint) SetPort(v *Port) { x.xxx_hidden_Port = v } func (x *NetworkEndpoint) SetHostname(v *Hostname) { x.xxx_hidden_Hostname = v } func (x *NetworkEndpoint) HasIpAddress() bool { if x == nil { return false } return x.xxx_hidden_IpAddress != nil } func (x *NetworkEndpoint) HasPort() bool { if x == nil { return false } return x.xxx_hidden_Port != nil } func (x *NetworkEndpoint) HasHostname() bool { if x == nil { return false } return x.xxx_hidden_Hostname != nil } func (x *NetworkEndpoint) ClearIpAddress() { x.xxx_hidden_IpAddress = nil } func (x *NetworkEndpoint) ClearPort() { x.xxx_hidden_Port = nil } func (x *NetworkEndpoint) ClearHostname() { x.xxx_hidden_Hostname = nil } type NetworkEndpoint_builder struct { _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. // Type of the network endpoint. Type NetworkEndpoint_Type // Optional IP address of a network endpoint. Must be specified when Type is // IP or IP_PORT. IpAddress *IpAddress // Optional port of a network endpoint. Must be specified when Type is IP_PORT // or HOSTNAME_PORT. Port *Port // Optional hostname of a network endpoint. Must be specified when Type is // HOSTNAME or HOSTNAME_PORT. Hostname *Hostname } func (b0 NetworkEndpoint_builder) Build() *NetworkEndpoint { m0 := &NetworkEndpoint{} b, x := &b0, m0 _, _ = b, x x.xxx_hidden_Type = b.Type x.xxx_hidden_IpAddress = b.IpAddress x.xxx_hidden_Port = b.Port x.xxx_hidden_Hostname = b.Hostname return m0 } var File_network_proto protoreflect.FileDescriptor const file_network_proto_rawDesc = "" + "\n" + "\rnetwork.proto\x12\rtsunami.proto\"j\n" + "\tIpAddress\x12C\n" + "\x0eaddress_family\x18\x01 \x01(\x0e2\x1c.tsunami.proto.AddressFamilyR\raddressFamily\x12\x18\n" + "\aaddress\x18\x02 \x01(\tR\aaddress\"'\n" + "\x04Port\x12\x1f\n" + "\vport_number\x18\x01 \x01(\rR\n" + "portNumber\"\x1e\n" + "\bHostname\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\"\xdc\x02\n" + "\x0fNetworkEndpoint\x127\n" + "\x04type\x18\x01 \x01(\x0e2#.tsunami.proto.NetworkEndpoint.TypeR\x04type\x127\n" + "\n" + "ip_address\x18\x02 \x01(\v2\x18.tsunami.proto.IpAddressR\tipAddress\x12'\n" + "\x04port\x18\x03 \x01(\v2\x13.tsunami.proto.PortR\x04port\x123\n" + "\bhostname\x18\x04 \x01(\v2\x17.tsunami.proto.HostnameR\bhostname\"y\n" + "\x04Type\x12\x14\n" + "\x10TYPE_UNSPECIFIED\x10\x00\x12\x06\n" + "\x02IP\x10\x01\x12\v\n" + "\aIP_PORT\x10\x02\x12\f\n" + "\bHOSTNAME\x10\x03\x12\x11\n" + "\rHOSTNAME_PORT\x10\x04\x12\x0f\n" + "\vIP_HOSTNAME\x10\x05\x12\x14\n" + "\x10IP_HOSTNAME_PORT\x10\x06*C\n" + "\rAddressFamily\x12\x1e\n" + "\x1aADDRESS_FAMILY_UNSPECIFIED\x10\x00\x12\b\n" + "\x04IPV4\x10\x04\x12\b\n" + "\x04IPV6\x10\x06*S\n" + "\x11TransportProtocol\x12\"\n" + "\x1eTRANSPORT_PROTOCOL_UNSPECIFIED\x10\x00\x12\a\n" + "\x03TCP\x10\x01\x12\a\n" + "\x03UDP\x10\x02\x12\b\n" + "\x04SCTP\x10\x03Bq\n" + "\x18com.google.tsunami.protoB\rNetworkProtosP\x01ZDgithub.com/google/tsunami-security-scanner/proto/go/network_go_protob\x06proto3" var file_network_proto_enumTypes = make([]protoimpl.EnumInfo, 3) var file_network_proto_msgTypes = make([]protoimpl.MessageInfo, 4) var file_network_proto_goTypes = []any{ (AddressFamily)(0), // 0: tsunami.proto.AddressFamily (TransportProtocol)(0), // 1: tsunami.proto.TransportProtocol (NetworkEndpoint_Type)(0), // 2: tsunami.proto.NetworkEndpoint.Type (*IpAddress)(nil), // 3: tsunami.proto.IpAddress (*Port)(nil), // 4: tsunami.proto.Port (*Hostname)(nil), // 5: tsunami.proto.Hostname (*NetworkEndpoint)(nil), // 6: tsunami.proto.NetworkEndpoint } var file_network_proto_depIdxs = []int32{ 0, // 0: tsunami.proto.IpAddress.address_family:type_name -> tsunami.proto.AddressFamily 2, // 1: tsunami.proto.NetworkEndpoint.type:type_name -> tsunami.proto.NetworkEndpoint.Type 3, // 2: tsunami.proto.NetworkEndpoint.ip_address:type_name -> tsunami.proto.IpAddress 4, // 3: tsunami.proto.NetworkEndpoint.port:type_name -> tsunami.proto.Port 5, // 4: tsunami.proto.NetworkEndpoint.hostname:type_name -> tsunami.proto.Hostname 5, // [5:5] is the sub-list for method output_type 5, // [5:5] is the sub-list for method input_type 5, // [5:5] is the sub-list for extension type_name 5, // [5:5] is the sub-list for extension extendee 0, // [0:5] is the sub-list for field type_name } func init() { file_network_proto_init() } func file_network_proto_init() { if File_network_proto != nil { return } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_network_proto_rawDesc), len(file_network_proto_rawDesc)), NumEnums: 3, NumMessages: 4, NumExtensions: 0, NumServices: 0, }, GoTypes: file_network_proto_goTypes, DependencyIndexes: file_network_proto_depIdxs, EnumInfos: file_network_proto_enumTypes, MessageInfos: file_network_proto_msgTypes, }.Build() File_network_proto = out.File file_network_proto_goTypes = nil file_network_proto_depIdxs = nil } ================================================ FILE: proto/go/network_service_go_proto/network_service.pb.go ================================================ // // Copyright 2019 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Data models for describing a network service. // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.11 // protoc v3.21.12 // source: network_service.proto package network_service_go_proto import ( network_go_proto "github.com/google/tsunami-security-scanner/proto/go/network_go_proto" software_go_proto "github.com/google/tsunami-security-scanner/proto/go/software_go_proto" web_crawl_go_proto "github.com/google/tsunami-security-scanner/proto/go/web_crawl_go_proto" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" unsafe "unsafe" ) const ( // Verify that this generated code is sufficiently up-to-date. _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) // Verify that runtime/protoimpl is sufficiently up-to-date. _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) // General information about a network service running on a target. type NetworkService struct { state protoimpl.MessageState `protogen:"opaque.v1"` xxx_hidden_NetworkEndpoint *network_go_proto.NetworkEndpoint `protobuf:"bytes,1,opt,name=network_endpoint,json=networkEndpoint,proto3"` xxx_hidden_TransportProtocol network_go_proto.TransportProtocol `protobuf:"varint,2,opt,name=transport_protocol,json=transportProtocol,proto3,enum=tsunami.proto.TransportProtocol"` xxx_hidden_ServiceName string `protobuf:"bytes,3,opt,name=service_name,json=serviceName,proto3"` xxx_hidden_Software *software_go_proto.Software `protobuf:"bytes,4,opt,name=software,proto3"` xxx_hidden_VersionSet *software_go_proto.VersionSet `protobuf:"bytes,5,opt,name=version_set,json=versionSet,proto3"` xxx_hidden_Banner []string `protobuf:"bytes,6,rep,name=banner,proto3"` xxx_hidden_ServiceContext *ServiceContext `protobuf:"bytes,7,opt,name=service_context,json=serviceContext,proto3"` xxx_hidden_Cpes []string `protobuf:"bytes,8,rep,name=cpes,proto3"` xxx_hidden_SupportedSslVersions []string `protobuf:"bytes,9,rep,name=supported_ssl_versions,json=supportedSslVersions,proto3"` xxx_hidden_SupportedHttpMethods []string `protobuf:"bytes,10,rep,name=supported_http_methods,json=supportedHttpMethods,proto3"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *NetworkService) Reset() { *x = NetworkService{} mi := &file_network_service_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *NetworkService) String() string { return protoimpl.X.MessageStringOf(x) } func (*NetworkService) ProtoMessage() {} func (x *NetworkService) ProtoReflect() protoreflect.Message { mi := &file_network_service_proto_msgTypes[0] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } func (x *NetworkService) GetNetworkEndpoint() *network_go_proto.NetworkEndpoint { if x != nil { return x.xxx_hidden_NetworkEndpoint } return nil } func (x *NetworkService) GetTransportProtocol() network_go_proto.TransportProtocol { if x != nil { return x.xxx_hidden_TransportProtocol } return network_go_proto.TransportProtocol(0) } func (x *NetworkService) GetServiceName() string { if x != nil { return x.xxx_hidden_ServiceName } return "" } func (x *NetworkService) GetSoftware() *software_go_proto.Software { if x != nil { return x.xxx_hidden_Software } return nil } func (x *NetworkService) GetVersionSet() *software_go_proto.VersionSet { if x != nil { return x.xxx_hidden_VersionSet } return nil } func (x *NetworkService) GetBanner() []string { if x != nil { return x.xxx_hidden_Banner } return nil } func (x *NetworkService) GetServiceContext() *ServiceContext { if x != nil { return x.xxx_hidden_ServiceContext } return nil } func (x *NetworkService) GetCpes() []string { if x != nil { return x.xxx_hidden_Cpes } return nil } func (x *NetworkService) GetSupportedSslVersions() []string { if x != nil { return x.xxx_hidden_SupportedSslVersions } return nil } func (x *NetworkService) GetSupportedHttpMethods() []string { if x != nil { return x.xxx_hidden_SupportedHttpMethods } return nil } func (x *NetworkService) SetNetworkEndpoint(v *network_go_proto.NetworkEndpoint) { x.xxx_hidden_NetworkEndpoint = v } func (x *NetworkService) SetTransportProtocol(v network_go_proto.TransportProtocol) { x.xxx_hidden_TransportProtocol = v } func (x *NetworkService) SetServiceName(v string) { x.xxx_hidden_ServiceName = v } func (x *NetworkService) SetSoftware(v *software_go_proto.Software) { x.xxx_hidden_Software = v } func (x *NetworkService) SetVersionSet(v *software_go_proto.VersionSet) { x.xxx_hidden_VersionSet = v } func (x *NetworkService) SetBanner(v []string) { x.xxx_hidden_Banner = v } func (x *NetworkService) SetServiceContext(v *ServiceContext) { x.xxx_hidden_ServiceContext = v } func (x *NetworkService) SetCpes(v []string) { x.xxx_hidden_Cpes = v } func (x *NetworkService) SetSupportedSslVersions(v []string) { x.xxx_hidden_SupportedSslVersions = v } func (x *NetworkService) SetSupportedHttpMethods(v []string) { x.xxx_hidden_SupportedHttpMethods = v } func (x *NetworkService) HasNetworkEndpoint() bool { if x == nil { return false } return x.xxx_hidden_NetworkEndpoint != nil } func (x *NetworkService) HasSoftware() bool { if x == nil { return false } return x.xxx_hidden_Software != nil } func (x *NetworkService) HasVersionSet() bool { if x == nil { return false } return x.xxx_hidden_VersionSet != nil } func (x *NetworkService) HasServiceContext() bool { if x == nil { return false } return x.xxx_hidden_ServiceContext != nil } func (x *NetworkService) ClearNetworkEndpoint() { x.xxx_hidden_NetworkEndpoint = nil } func (x *NetworkService) ClearSoftware() { x.xxx_hidden_Software = nil } func (x *NetworkService) ClearVersionSet() { x.xxx_hidden_VersionSet = nil } func (x *NetworkService) ClearServiceContext() { x.xxx_hidden_ServiceContext = nil } type NetworkService_builder struct { _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. // The network endpoint where this network service is served. NetworkEndpoint *network_go_proto.NetworkEndpoint // The transport layer protocol used by the service. TransportProtocol network_go_proto.TransportProtocol // The name of the network service, following convention in RFC6335. Examples // are like http, telnet, ssh, etc. ServiceName string // The software that provides the service behind the port. Software *software_go_proto.Software // The complete set of versions of the software. VersionSet *software_go_proto.VersionSet // Banners generated by the service. Banner []string // Context information about this network service. ServiceContext *ServiceContext // The detected Common Platform Enumeration (CPE) name for service, // in the uri binding representation, like: cpe:/a:openbsd:openssh:8.4p1 Cpes []string // List of supported SSL versions (e.g. TLSv1, SSLv3, ...) on the service. SupportedSslVersions []string // List of supported HTTP methods (e.g. POST, GET, ...) on the service. SupportedHttpMethods []string } func (b0 NetworkService_builder) Build() *NetworkService { m0 := &NetworkService{} b, x := &b0, m0 _, _ = b, x x.xxx_hidden_NetworkEndpoint = b.NetworkEndpoint x.xxx_hidden_TransportProtocol = b.TransportProtocol x.xxx_hidden_ServiceName = b.ServiceName x.xxx_hidden_Software = b.Software x.xxx_hidden_VersionSet = b.VersionSet x.xxx_hidden_Banner = b.Banner x.xxx_hidden_ServiceContext = b.ServiceContext x.xxx_hidden_Cpes = b.Cpes x.xxx_hidden_SupportedSslVersions = b.SupportedSslVersions x.xxx_hidden_SupportedHttpMethods = b.SupportedHttpMethods return m0 } // Context information about a specific network service. type ServiceContext struct { state protoimpl.MessageState `protogen:"opaque.v1"` xxx_hidden_Context isServiceContext_Context `protobuf_oneof:"context"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *ServiceContext) Reset() { *x = ServiceContext{} mi := &file_network_service_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *ServiceContext) String() string { return protoimpl.X.MessageStringOf(x) } func (*ServiceContext) ProtoMessage() {} func (x *ServiceContext) ProtoReflect() protoreflect.Message { mi := &file_network_service_proto_msgTypes[1] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } func (x *ServiceContext) GetWebServiceContext() *WebServiceContext { if x != nil { if x, ok := x.xxx_hidden_Context.(*serviceContext_WebServiceContext); ok { return x.WebServiceContext } } return nil } func (x *ServiceContext) SetWebServiceContext(v *WebServiceContext) { if v == nil { x.xxx_hidden_Context = nil return } x.xxx_hidden_Context = &serviceContext_WebServiceContext{v} } func (x *ServiceContext) HasContext() bool { if x == nil { return false } return x.xxx_hidden_Context != nil } func (x *ServiceContext) HasWebServiceContext() bool { if x == nil { return false } _, ok := x.xxx_hidden_Context.(*serviceContext_WebServiceContext) return ok } func (x *ServiceContext) ClearContext() { x.xxx_hidden_Context = nil } func (x *ServiceContext) ClearWebServiceContext() { if _, ok := x.xxx_hidden_Context.(*serviceContext_WebServiceContext); ok { x.xxx_hidden_Context = nil } } const ServiceContext_Context_not_set_case case_ServiceContext_Context = 0 const ServiceContext_WebServiceContext_case case_ServiceContext_Context = 1 func (x *ServiceContext) WhichContext() case_ServiceContext_Context { if x == nil { return ServiceContext_Context_not_set_case } switch x.xxx_hidden_Context.(type) { case *serviceContext_WebServiceContext: return ServiceContext_WebServiceContext_case default: return ServiceContext_Context_not_set_case } } type ServiceContext_builder struct { _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. // Fields of oneof xxx_hidden_Context: WebServiceContext *WebServiceContext // -- end of xxx_hidden_Context } func (b0 ServiceContext_builder) Build() *ServiceContext { m0 := &ServiceContext{} b, x := &b0, m0 _, _ = b, x if b.WebServiceContext != nil { x.xxx_hidden_Context = &serviceContext_WebServiceContext{b.WebServiceContext} } return m0 } type case_ServiceContext_Context protoreflect.FieldNumber func (x case_ServiceContext_Context) String() string { md := file_network_service_proto_msgTypes[1].Descriptor() if x == 0 { return "not set" } return protoimpl.X.MessageFieldStringOf(md, protoreflect.FieldNumber(x)) } type isServiceContext_Context interface { isServiceContext_Context() } type serviceContext_WebServiceContext struct { WebServiceContext *WebServiceContext `protobuf:"bytes,1,opt,name=web_service_context,json=webServiceContext,proto3,oneof"` } func (*serviceContext_WebServiceContext) isServiceContext_Context() {} // Context information about a web application. // NEXT ID: 5 type WebServiceContext struct { state protoimpl.MessageState `protogen:"opaque.v1"` xxx_hidden_ApplicationRoot string `protobuf:"bytes,1,opt,name=application_root,json=applicationRoot,proto3"` xxx_hidden_Software *software_go_proto.Software `protobuf:"bytes,2,opt,name=software,proto3"` xxx_hidden_VersionSet *software_go_proto.VersionSet `protobuf:"bytes,3,opt,name=version_set,json=versionSet,proto3"` xxx_hidden_CrawlResults *[]*web_crawl_go_proto.CrawlResult `protobuf:"bytes,4,rep,name=crawl_results,json=crawlResults,proto3"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *WebServiceContext) Reset() { *x = WebServiceContext{} mi := &file_network_service_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *WebServiceContext) String() string { return protoimpl.X.MessageStringOf(x) } func (*WebServiceContext) ProtoMessage() {} func (x *WebServiceContext) ProtoReflect() protoreflect.Message { mi := &file_network_service_proto_msgTypes[2] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } func (x *WebServiceContext) GetApplicationRoot() string { if x != nil { return x.xxx_hidden_ApplicationRoot } return "" } func (x *WebServiceContext) GetSoftware() *software_go_proto.Software { if x != nil { return x.xxx_hidden_Software } return nil } func (x *WebServiceContext) GetVersionSet() *software_go_proto.VersionSet { if x != nil { return x.xxx_hidden_VersionSet } return nil } func (x *WebServiceContext) GetCrawlResults() []*web_crawl_go_proto.CrawlResult { if x != nil { if x.xxx_hidden_CrawlResults != nil { return *x.xxx_hidden_CrawlResults } } return nil } func (x *WebServiceContext) SetApplicationRoot(v string) { x.xxx_hidden_ApplicationRoot = v } func (x *WebServiceContext) SetSoftware(v *software_go_proto.Software) { x.xxx_hidden_Software = v } func (x *WebServiceContext) SetVersionSet(v *software_go_proto.VersionSet) { x.xxx_hidden_VersionSet = v } func (x *WebServiceContext) SetCrawlResults(v []*web_crawl_go_proto.CrawlResult) { x.xxx_hidden_CrawlResults = &v } func (x *WebServiceContext) HasSoftware() bool { if x == nil { return false } return x.xxx_hidden_Software != nil } func (x *WebServiceContext) HasVersionSet() bool { if x == nil { return false } return x.xxx_hidden_VersionSet != nil } func (x *WebServiceContext) ClearSoftware() { x.xxx_hidden_Software = nil } func (x *WebServiceContext) ClearVersionSet() { x.xxx_hidden_VersionSet = nil } type WebServiceContext_builder struct { _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. // The root path of the hosted web application. ApplicationRoot string // The web application that is serving under the application root. Software *software_go_proto.Software // The detected versions of the web application. VersionSet *software_go_proto.VersionSet // Fingerprinter's crawling results for this web service. CrawlResults []*web_crawl_go_proto.CrawlResult } func (b0 WebServiceContext_builder) Build() *WebServiceContext { m0 := &WebServiceContext{} b, x := &b0, m0 _, _ = b, x x.xxx_hidden_ApplicationRoot = b.ApplicationRoot x.xxx_hidden_Software = b.Software x.xxx_hidden_VersionSet = b.VersionSet x.xxx_hidden_CrawlResults = &b.CrawlResults return m0 } var File_network_service_proto protoreflect.FileDescriptor const file_network_service_proto_rawDesc = "" + "\n" + "\x15network_service.proto\x12\rtsunami.proto\x1a\rnetwork.proto\x1a\x0esoftware.proto\x1a\x0fweb_crawl.proto\"\xa0\x04\n" + "\x0eNetworkService\x12I\n" + "\x10network_endpoint\x18\x01 \x01(\v2\x1e.tsunami.proto.NetworkEndpointR\x0fnetworkEndpoint\x12O\n" + "\x12transport_protocol\x18\x02 \x01(\x0e2 .tsunami.proto.TransportProtocolR\x11transportProtocol\x12!\n" + "\fservice_name\x18\x03 \x01(\tR\vserviceName\x123\n" + "\bsoftware\x18\x04 \x01(\v2\x17.tsunami.proto.SoftwareR\bsoftware\x12:\n" + "\vversion_set\x18\x05 \x01(\v2\x19.tsunami.proto.VersionSetR\n" + "versionSet\x12\x16\n" + "\x06banner\x18\x06 \x03(\tR\x06banner\x12F\n" + "\x0fservice_context\x18\a \x01(\v2\x1d.tsunami.proto.ServiceContextR\x0eserviceContext\x12\x12\n" + "\x04cpes\x18\b \x03(\tR\x04cpes\x124\n" + "\x16supported_ssl_versions\x18\t \x03(\tR\x14supportedSslVersions\x124\n" + "\x16supported_http_methods\x18\n" + " \x03(\tR\x14supportedHttpMethods\"o\n" + "\x0eServiceContext\x12R\n" + "\x13web_service_context\x18\x01 \x01(\v2 .tsunami.proto.WebServiceContextH\x00R\x11webServiceContextB\t\n" + "\acontext\"\xf0\x01\n" + "\x11WebServiceContext\x12)\n" + "\x10application_root\x18\x01 \x01(\tR\x0fapplicationRoot\x123\n" + "\bsoftware\x18\x02 \x01(\v2\x17.tsunami.proto.SoftwareR\bsoftware\x12:\n" + "\vversion_set\x18\x03 \x01(\v2\x19.tsunami.proto.VersionSetR\n" + "versionSet\x12?\n" + "\rcrawl_results\x18\x04 \x03(\v2\x1a.tsunami.proto.CrawlResultR\fcrawlResultsB\x80\x01\n" + "\x18com.google.tsunami.protoB\x14NetworkServiceProtosP\x01ZLgithub.com/google/tsunami-security-scanner/proto/go/network_service_go_protob\x06proto3" var file_network_service_proto_msgTypes = make([]protoimpl.MessageInfo, 3) var file_network_service_proto_goTypes = []any{ (*NetworkService)(nil), // 0: tsunami.proto.NetworkService (*ServiceContext)(nil), // 1: tsunami.proto.ServiceContext (*WebServiceContext)(nil), // 2: tsunami.proto.WebServiceContext (*network_go_proto.NetworkEndpoint)(nil), // 3: tsunami.proto.NetworkEndpoint (network_go_proto.TransportProtocol)(0), // 4: tsunami.proto.TransportProtocol (*software_go_proto.Software)(nil), // 5: tsunami.proto.Software (*software_go_proto.VersionSet)(nil), // 6: tsunami.proto.VersionSet (*web_crawl_go_proto.CrawlResult)(nil), // 7: tsunami.proto.CrawlResult } var file_network_service_proto_depIdxs = []int32{ 3, // 0: tsunami.proto.NetworkService.network_endpoint:type_name -> tsunami.proto.NetworkEndpoint 4, // 1: tsunami.proto.NetworkService.transport_protocol:type_name -> tsunami.proto.TransportProtocol 5, // 2: tsunami.proto.NetworkService.software:type_name -> tsunami.proto.Software 6, // 3: tsunami.proto.NetworkService.version_set:type_name -> tsunami.proto.VersionSet 1, // 4: tsunami.proto.NetworkService.service_context:type_name -> tsunami.proto.ServiceContext 2, // 5: tsunami.proto.ServiceContext.web_service_context:type_name -> tsunami.proto.WebServiceContext 5, // 6: tsunami.proto.WebServiceContext.software:type_name -> tsunami.proto.Software 6, // 7: tsunami.proto.WebServiceContext.version_set:type_name -> tsunami.proto.VersionSet 7, // 8: tsunami.proto.WebServiceContext.crawl_results:type_name -> tsunami.proto.CrawlResult 9, // [9:9] is the sub-list for method output_type 9, // [9:9] is the sub-list for method input_type 9, // [9:9] is the sub-list for extension type_name 9, // [9:9] is the sub-list for extension extendee 0, // [0:9] is the sub-list for field type_name } func init() { file_network_service_proto_init() } func file_network_service_proto_init() { if File_network_service_proto != nil { return } file_network_service_proto_msgTypes[1].OneofWrappers = []any{ (*serviceContext_WebServiceContext)(nil), } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_network_service_proto_rawDesc), len(file_network_service_proto_rawDesc)), NumEnums: 0, NumMessages: 3, NumExtensions: 0, NumServices: 0, }, GoTypes: file_network_service_proto_goTypes, DependencyIndexes: file_network_service_proto_depIdxs, MessageInfos: file_network_service_proto_msgTypes, }.Build() File_network_service_proto = out.File file_network_service_proto_goTypes = nil file_network_service_proto_depIdxs = nil } ================================================ FILE: proto/go/payload_generator_go_proto/payload_generator.pb.go ================================================ // // Copyright 2022 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Data models utilized by the Tsunami Paylaod Generator // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.11 // protoc v3.21.12 // source: payload_generator.proto package payload_generator_go_proto import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" wrapperspb "google.golang.org/protobuf/types/known/wrapperspb" reflect "reflect" unsafe "unsafe" ) const ( // Verify that this generated code is sufficiently up-to-date. _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) // Verify that runtime/protoimpl is sufficiently up-to-date. _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) type PayloadValidationType int32 const ( PayloadValidationType_VALIDATION_TYPE_UNSPECIFIED PayloadValidationType = 0 PayloadValidationType_VALIDATION_REGEX PayloadValidationType = 1 ) // Enum value maps for PayloadValidationType. var ( PayloadValidationType_name = map[int32]string{ 0: "VALIDATION_TYPE_UNSPECIFIED", 1: "VALIDATION_REGEX", } PayloadValidationType_value = map[string]int32{ "VALIDATION_TYPE_UNSPECIFIED": 0, "VALIDATION_REGEX": 1, } ) func (x PayloadValidationType) Enum() *PayloadValidationType { p := new(PayloadValidationType) *p = x return p } func (x PayloadValidationType) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (PayloadValidationType) Descriptor() protoreflect.EnumDescriptor { return file_payload_generator_proto_enumTypes[0].Descriptor() } func (PayloadValidationType) Type() protoreflect.EnumType { return &file_payload_generator_proto_enumTypes[0] } func (x PayloadValidationType) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } // The type of vulnerability the detector is looking for type PayloadGeneratorConfig_VulnerabilityType int32 const ( // Unspecified vulnerability type PayloadGeneratorConfig_VULNERABILITY_TYPE_UNSPECIFIED PayloadGeneratorConfig_VulnerabilityType = 0 // RCE which returns the output of the execution PayloadGeneratorConfig_REFLECTIVE_RCE PayloadGeneratorConfig_VulnerabilityType = 1 // RCE which does not return the output of the execution PayloadGeneratorConfig_BLIND_RCE PayloadGeneratorConfig_VulnerabilityType = 2 // Server-Side Request Forgery PayloadGeneratorConfig_SSRF PayloadGeneratorConfig_VulnerabilityType = 3 // Arbitrary File Write PayloadGeneratorConfig_ARBITRARY_FILE_WRITE PayloadGeneratorConfig_VulnerabilityType = 4 // RCE without output of the execution + File Read (needed to get // confirmation string) PayloadGeneratorConfig_BLIND_RCE_FILE_READ PayloadGeneratorConfig_VulnerabilityType = 5 ) // Enum value maps for PayloadGeneratorConfig_VulnerabilityType. var ( PayloadGeneratorConfig_VulnerabilityType_name = map[int32]string{ 0: "VULNERABILITY_TYPE_UNSPECIFIED", 1: "REFLECTIVE_RCE", 2: "BLIND_RCE", 3: "SSRF", 4: "ARBITRARY_FILE_WRITE", 5: "BLIND_RCE_FILE_READ", } PayloadGeneratorConfig_VulnerabilityType_value = map[string]int32{ "VULNERABILITY_TYPE_UNSPECIFIED": 0, "REFLECTIVE_RCE": 1, "BLIND_RCE": 2, "SSRF": 3, "ARBITRARY_FILE_WRITE": 4, "BLIND_RCE_FILE_READ": 5, } ) func (x PayloadGeneratorConfig_VulnerabilityType) Enum() *PayloadGeneratorConfig_VulnerabilityType { p := new(PayloadGeneratorConfig_VulnerabilityType) *p = x return p } func (x PayloadGeneratorConfig_VulnerabilityType) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (PayloadGeneratorConfig_VulnerabilityType) Descriptor() protoreflect.EnumDescriptor { return file_payload_generator_proto_enumTypes[1].Descriptor() } func (PayloadGeneratorConfig_VulnerabilityType) Type() protoreflect.EnumType { return &file_payload_generator_proto_enumTypes[1] } func (x PayloadGeneratorConfig_VulnerabilityType) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } // The environment that processes the payload for execution e.g. a PHP-based // target likely wants a payload that is itself PHP code. type PayloadGeneratorConfig_InterpretationEnvironment int32 const ( // Unspecified interpretation environment type PayloadGeneratorConfig_INTERPRETATION_ENVIRONMENT_UNSPECIFIED PayloadGeneratorConfig_InterpretationEnvironment = 0 // Payload is interpreted within a Linux shell environment PayloadGeneratorConfig_LINUX_SHELL PayloadGeneratorConfig_InterpretationEnvironment = 1 // Payload is interpreted wihin a Java compiler context PayloadGeneratorConfig_JAVA PayloadGeneratorConfig_InterpretationEnvironment = 2 // Payload is interpreted wihin a PHP VM context PayloadGeneratorConfig_PHP PayloadGeneratorConfig_InterpretationEnvironment = 3 // Interpretation environment doesn't matter PayloadGeneratorConfig_INTERPRETATION_ANY PayloadGeneratorConfig_InterpretationEnvironment = 4 // Payload is interpreted wihin crontab PayloadGeneratorConfig_LINUX_ROOT_CRONTAB PayloadGeneratorConfig_InterpretationEnvironment = 5 // Payload is interpreted wihin a Windows shell environment PayloadGeneratorConfig_WINDOWS_SHELL PayloadGeneratorConfig_InterpretationEnvironment = 6 // Payload is interpreted within a JSP shell environment PayloadGeneratorConfig_JSP PayloadGeneratorConfig_InterpretationEnvironment = 7 ) // Enum value maps for PayloadGeneratorConfig_InterpretationEnvironment. var ( PayloadGeneratorConfig_InterpretationEnvironment_name = map[int32]string{ 0: "INTERPRETATION_ENVIRONMENT_UNSPECIFIED", 1: "LINUX_SHELL", 2: "JAVA", 3: "PHP", 4: "INTERPRETATION_ANY", 5: "LINUX_ROOT_CRONTAB", 6: "WINDOWS_SHELL", 7: "JSP", } PayloadGeneratorConfig_InterpretationEnvironment_value = map[string]int32{ "INTERPRETATION_ENVIRONMENT_UNSPECIFIED": 0, "LINUX_SHELL": 1, "JAVA": 2, "PHP": 3, "INTERPRETATION_ANY": 4, "LINUX_ROOT_CRONTAB": 5, "WINDOWS_SHELL": 6, "JSP": 7, } ) func (x PayloadGeneratorConfig_InterpretationEnvironment) Enum() *PayloadGeneratorConfig_InterpretationEnvironment { p := new(PayloadGeneratorConfig_InterpretationEnvironment) *p = x return p } func (x PayloadGeneratorConfig_InterpretationEnvironment) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (PayloadGeneratorConfig_InterpretationEnvironment) Descriptor() protoreflect.EnumDescriptor { return file_payload_generator_proto_enumTypes[2].Descriptor() } func (PayloadGeneratorConfig_InterpretationEnvironment) Type() protoreflect.EnumType { return &file_payload_generator_proto_enumTypes[2] } func (x PayloadGeneratorConfig_InterpretationEnvironment) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } // The actual runtime environment when the payload is run e.g. while a // PHP-based target wants a PHP-interpretation environment, the actual code // execution may happen via the Linux shell: exec(“echo \”this is running in // the system.\””). type PayloadGeneratorConfig_ExecutionEnvironment int32 const ( // Unspecified execution environment type PayloadGeneratorConfig_EXECUTION_ENVIRONMENT_UNSPECIFIED PayloadGeneratorConfig_ExecutionEnvironment = 0 // Execute within the InterpretationEnvironment PayloadGeneratorConfig_EXEC_INTERPRETATION_ENVIRONMENT PayloadGeneratorConfig_ExecutionEnvironment = 1 // Execution environment doesn't matter PayloadGeneratorConfig_EXEC_ANY PayloadGeneratorConfig_ExecutionEnvironment = 2 ) // Enum value maps for PayloadGeneratorConfig_ExecutionEnvironment. var ( PayloadGeneratorConfig_ExecutionEnvironment_name = map[int32]string{ 0: "EXECUTION_ENVIRONMENT_UNSPECIFIED", 1: "EXEC_INTERPRETATION_ENVIRONMENT", 2: "EXEC_ANY", } PayloadGeneratorConfig_ExecutionEnvironment_value = map[string]int32{ "EXECUTION_ENVIRONMENT_UNSPECIFIED": 0, "EXEC_INTERPRETATION_ENVIRONMENT": 1, "EXEC_ANY": 2, } ) func (x PayloadGeneratorConfig_ExecutionEnvironment) Enum() *PayloadGeneratorConfig_ExecutionEnvironment { p := new(PayloadGeneratorConfig_ExecutionEnvironment) *p = x return p } func (x PayloadGeneratorConfig_ExecutionEnvironment) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (PayloadGeneratorConfig_ExecutionEnvironment) Descriptor() protoreflect.EnumDescriptor { return file_payload_generator_proto_enumTypes[3].Descriptor() } func (PayloadGeneratorConfig_ExecutionEnvironment) Type() protoreflect.EnumType { return &file_payload_generator_proto_enumTypes[3] } func (x PayloadGeneratorConfig_ExecutionEnvironment) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } // Attributes utilized by the PayloadGenerator to select a payload type PayloadGeneratorConfig struct { state protoimpl.MessageState `protogen:"opaque.v1"` xxx_hidden_VulnerabilityType PayloadGeneratorConfig_VulnerabilityType `protobuf:"varint,2,opt,name=vulnerability_type,json=vulnerabilityType,proto3,enum=tsunami.proto.PayloadGeneratorConfig_VulnerabilityType"` xxx_hidden_InterpretationEnvironment PayloadGeneratorConfig_InterpretationEnvironment `protobuf:"varint,3,opt,name=interpretation_environment,json=interpretationEnvironment,proto3,enum=tsunami.proto.PayloadGeneratorConfig_InterpretationEnvironment"` xxx_hidden_ExecutionEnvironment PayloadGeneratorConfig_ExecutionEnvironment `protobuf:"varint,4,opt,name=execution_environment,json=executionEnvironment,proto3,enum=tsunami.proto.PayloadGeneratorConfig_ExecutionEnvironment"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *PayloadGeneratorConfig) Reset() { *x = PayloadGeneratorConfig{} mi := &file_payload_generator_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *PayloadGeneratorConfig) String() string { return protoimpl.X.MessageStringOf(x) } func (*PayloadGeneratorConfig) ProtoMessage() {} func (x *PayloadGeneratorConfig) ProtoReflect() protoreflect.Message { mi := &file_payload_generator_proto_msgTypes[0] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } func (x *PayloadGeneratorConfig) GetVulnerabilityType() PayloadGeneratorConfig_VulnerabilityType { if x != nil { return x.xxx_hidden_VulnerabilityType } return PayloadGeneratorConfig_VULNERABILITY_TYPE_UNSPECIFIED } func (x *PayloadGeneratorConfig) GetInterpretationEnvironment() PayloadGeneratorConfig_InterpretationEnvironment { if x != nil { return x.xxx_hidden_InterpretationEnvironment } return PayloadGeneratorConfig_INTERPRETATION_ENVIRONMENT_UNSPECIFIED } func (x *PayloadGeneratorConfig) GetExecutionEnvironment() PayloadGeneratorConfig_ExecutionEnvironment { if x != nil { return x.xxx_hidden_ExecutionEnvironment } return PayloadGeneratorConfig_EXECUTION_ENVIRONMENT_UNSPECIFIED } func (x *PayloadGeneratorConfig) SetVulnerabilityType(v PayloadGeneratorConfig_VulnerabilityType) { x.xxx_hidden_VulnerabilityType = v } func (x *PayloadGeneratorConfig) SetInterpretationEnvironment(v PayloadGeneratorConfig_InterpretationEnvironment) { x.xxx_hidden_InterpretationEnvironment = v } func (x *PayloadGeneratorConfig) SetExecutionEnvironment(v PayloadGeneratorConfig_ExecutionEnvironment) { x.xxx_hidden_ExecutionEnvironment = v } type PayloadGeneratorConfig_builder struct { _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. VulnerabilityType PayloadGeneratorConfig_VulnerabilityType InterpretationEnvironment PayloadGeneratorConfig_InterpretationEnvironment ExecutionEnvironment PayloadGeneratorConfig_ExecutionEnvironment } func (b0 PayloadGeneratorConfig_builder) Build() *PayloadGeneratorConfig { m0 := &PayloadGeneratorConfig{} b, x := &b0, m0 _, _ = b, x x.xxx_hidden_VulnerabilityType = b.VulnerabilityType x.xxx_hidden_InterpretationEnvironment = b.InterpretationEnvironment x.xxx_hidden_ExecutionEnvironment = b.ExecutionEnvironment return m0 } // Attributes of a payload. A detector can check these attributes to change its // logic based on the payload type. type PayloadAttributes struct { state protoimpl.MessageState `protogen:"opaque.v1"` xxx_hidden_UsesCallbackServer bool `protobuf:"varint,1,opt,name=uses_callback_server,json=usesCallbackServer,proto3"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *PayloadAttributes) Reset() { *x = PayloadAttributes{} mi := &file_payload_generator_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *PayloadAttributes) String() string { return protoimpl.X.MessageStringOf(x) } func (*PayloadAttributes) ProtoMessage() {} func (x *PayloadAttributes) ProtoReflect() protoreflect.Message { mi := &file_payload_generator_proto_msgTypes[1] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } func (x *PayloadAttributes) GetUsesCallbackServer() bool { if x != nil { return x.xxx_hidden_UsesCallbackServer } return false } func (x *PayloadAttributes) SetUsesCallbackServer(v bool) { x.xxx_hidden_UsesCallbackServer = v } type PayloadAttributes_builder struct { _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. // Whether the payload uses the callback server UsesCallbackServer bool } func (b0 PayloadAttributes_builder) Build() *PayloadAttributes { m0 := &PayloadAttributes{} b, x := &b0, m0 _, _ = b, x x.xxx_hidden_UsesCallbackServer = b.UsesCallbackServer return m0 } // Container type for payload_definitions.yaml type PayloadLibrary struct { state protoimpl.MessageState `protogen:"opaque.v1"` xxx_hidden_Payloads *[]*PayloadDefinition `protobuf:"bytes,1,rep,name=payloads,proto3"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *PayloadLibrary) Reset() { *x = PayloadLibrary{} mi := &file_payload_generator_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *PayloadLibrary) String() string { return protoimpl.X.MessageStringOf(x) } func (*PayloadLibrary) ProtoMessage() {} func (x *PayloadLibrary) ProtoReflect() protoreflect.Message { mi := &file_payload_generator_proto_msgTypes[2] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } func (x *PayloadLibrary) GetPayloads() []*PayloadDefinition { if x != nil { if x.xxx_hidden_Payloads != nil { return *x.xxx_hidden_Payloads } } return nil } func (x *PayloadLibrary) SetPayloads(v []*PayloadDefinition) { x.xxx_hidden_Payloads = &v } type PayloadLibrary_builder struct { _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. Payloads []*PayloadDefinition } func (b0 PayloadLibrary_builder) Build() *PayloadLibrary { m0 := &PayloadLibrary{} b, x := &b0, m0 _, _ = b, x x.xxx_hidden_Payloads = &b.Payloads return m0 } // Schema for each entry in payload_definitions.yaml // Note: this message uses StringValue and BoolValue because we validate whether // each payload definition in the yaml file has the correct fields present. // Since empty proto fields are given default values (proto fields are not // nullable), we use the wrapped types to check for actual presence. type PayloadDefinition struct { state protoimpl.MessageState `protogen:"opaque.v1"` xxx_hidden_Name *wrapperspb.StringValue `protobuf:"bytes,1,opt,name=name,proto3"` xxx_hidden_InterpretationEnvironment PayloadGeneratorConfig_InterpretationEnvironment `protobuf:"varint,2,opt,name=interpretation_environment,json=interpretationEnvironment,proto3,enum=tsunami.proto.PayloadGeneratorConfig_InterpretationEnvironment"` xxx_hidden_ExecutionEnvironment PayloadGeneratorConfig_ExecutionEnvironment `protobuf:"varint,3,opt,name=execution_environment,json=executionEnvironment,proto3,enum=tsunami.proto.PayloadGeneratorConfig_ExecutionEnvironment"` xxx_hidden_VulnerabilityType []PayloadGeneratorConfig_VulnerabilityType `protobuf:"varint,4,rep,packed,name=vulnerability_type,json=vulnerabilityType,proto3,enum=tsunami.proto.PayloadGeneratorConfig_VulnerabilityType"` xxx_hidden_UsesCallbackServer *wrapperspb.BoolValue `protobuf:"bytes,5,opt,name=uses_callback_server,json=usesCallbackServer,proto3"` xxx_hidden_PayloadString *wrapperspb.StringValue `protobuf:"bytes,6,opt,name=payload_string,json=payloadString,proto3"` xxx_hidden_ValidationType PayloadValidationType `protobuf:"varint,7,opt,name=validation_type,json=validationType,proto3,enum=tsunami.proto.PayloadValidationType"` xxx_hidden_ValidationRegex *wrapperspb.StringValue `protobuf:"bytes,8,opt,name=validation_regex,json=validationRegex,proto3"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *PayloadDefinition) Reset() { *x = PayloadDefinition{} mi := &file_payload_generator_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *PayloadDefinition) String() string { return protoimpl.X.MessageStringOf(x) } func (*PayloadDefinition) ProtoMessage() {} func (x *PayloadDefinition) ProtoReflect() protoreflect.Message { mi := &file_payload_generator_proto_msgTypes[3] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } func (x *PayloadDefinition) GetName() *wrapperspb.StringValue { if x != nil { return x.xxx_hidden_Name } return nil } func (x *PayloadDefinition) GetInterpretationEnvironment() PayloadGeneratorConfig_InterpretationEnvironment { if x != nil { return x.xxx_hidden_InterpretationEnvironment } return PayloadGeneratorConfig_INTERPRETATION_ENVIRONMENT_UNSPECIFIED } func (x *PayloadDefinition) GetExecutionEnvironment() PayloadGeneratorConfig_ExecutionEnvironment { if x != nil { return x.xxx_hidden_ExecutionEnvironment } return PayloadGeneratorConfig_EXECUTION_ENVIRONMENT_UNSPECIFIED } func (x *PayloadDefinition) GetVulnerabilityType() []PayloadGeneratorConfig_VulnerabilityType { if x != nil { return x.xxx_hidden_VulnerabilityType } return nil } func (x *PayloadDefinition) GetUsesCallbackServer() *wrapperspb.BoolValue { if x != nil { return x.xxx_hidden_UsesCallbackServer } return nil } func (x *PayloadDefinition) GetPayloadString() *wrapperspb.StringValue { if x != nil { return x.xxx_hidden_PayloadString } return nil } func (x *PayloadDefinition) GetValidationType() PayloadValidationType { if x != nil { return x.xxx_hidden_ValidationType } return PayloadValidationType_VALIDATION_TYPE_UNSPECIFIED } func (x *PayloadDefinition) GetValidationRegex() *wrapperspb.StringValue { if x != nil { return x.xxx_hidden_ValidationRegex } return nil } func (x *PayloadDefinition) SetName(v *wrapperspb.StringValue) { x.xxx_hidden_Name = v } func (x *PayloadDefinition) SetInterpretationEnvironment(v PayloadGeneratorConfig_InterpretationEnvironment) { x.xxx_hidden_InterpretationEnvironment = v } func (x *PayloadDefinition) SetExecutionEnvironment(v PayloadGeneratorConfig_ExecutionEnvironment) { x.xxx_hidden_ExecutionEnvironment = v } func (x *PayloadDefinition) SetVulnerabilityType(v []PayloadGeneratorConfig_VulnerabilityType) { x.xxx_hidden_VulnerabilityType = v } func (x *PayloadDefinition) SetUsesCallbackServer(v *wrapperspb.BoolValue) { x.xxx_hidden_UsesCallbackServer = v } func (x *PayloadDefinition) SetPayloadString(v *wrapperspb.StringValue) { x.xxx_hidden_PayloadString = v } func (x *PayloadDefinition) SetValidationType(v PayloadValidationType) { x.xxx_hidden_ValidationType = v } func (x *PayloadDefinition) SetValidationRegex(v *wrapperspb.StringValue) { x.xxx_hidden_ValidationRegex = v } func (x *PayloadDefinition) HasName() bool { if x == nil { return false } return x.xxx_hidden_Name != nil } func (x *PayloadDefinition) HasUsesCallbackServer() bool { if x == nil { return false } return x.xxx_hidden_UsesCallbackServer != nil } func (x *PayloadDefinition) HasPayloadString() bool { if x == nil { return false } return x.xxx_hidden_PayloadString != nil } func (x *PayloadDefinition) HasValidationRegex() bool { if x == nil { return false } return x.xxx_hidden_ValidationRegex != nil } func (x *PayloadDefinition) ClearName() { x.xxx_hidden_Name = nil } func (x *PayloadDefinition) ClearUsesCallbackServer() { x.xxx_hidden_UsesCallbackServer = nil } func (x *PayloadDefinition) ClearPayloadString() { x.xxx_hidden_PayloadString = nil } func (x *PayloadDefinition) ClearValidationRegex() { x.xxx_hidden_ValidationRegex = nil } type PayloadDefinition_builder struct { _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. // The human-readable string to identify the payload Name *wrapperspb.StringValue InterpretationEnvironment PayloadGeneratorConfig_InterpretationEnvironment ExecutionEnvironment PayloadGeneratorConfig_ExecutionEnvironment // All vulnerability types this payload can be used for VulnerabilityType []PayloadGeneratorConfig_VulnerabilityType // If true, payload_string must contain the $TSUNAMI_PAYLOAD_TOKEN_URL // token. Validation will automatically check against the callback server, so // the validation* fields do not need to be set. UsesCallbackServer *wrapperspb.BoolValue // The actual payload command string. The following special tokens can be // used which will cause the framework to inject dynamic content into the // command: // - $TSUNAMI_PAYLOAD_TOKEN_URL: url for the callback server // - a random string, used to reduce false positives. PayloadString *wrapperspb.StringValue // The type of validation function for determining if the payload was // executed. Currently, only REGEX is supported. ValidationType PayloadValidationType // Required if validation_type == REGEX. Must be compatible with // java.util.regex.Pattern. The string will first be preprocessed before // applied as a regex, replacing any of the following tokens with the // corresponding values supplied by the framework: // - $TSUNAMI_PAYLOAD_TOKEN_RANDOM: a random string, used to reduce false // positives. The value is guaranteed to be the same as the value supplied // to payload_string. ValidationRegex *wrapperspb.StringValue } func (b0 PayloadDefinition_builder) Build() *PayloadDefinition { m0 := &PayloadDefinition{} b, x := &b0, m0 _, _ = b, x x.xxx_hidden_Name = b.Name x.xxx_hidden_InterpretationEnvironment = b.InterpretationEnvironment x.xxx_hidden_ExecutionEnvironment = b.ExecutionEnvironment x.xxx_hidden_VulnerabilityType = b.VulnerabilityType x.xxx_hidden_UsesCallbackServer = b.UsesCallbackServer x.xxx_hidden_PayloadString = b.PayloadString x.xxx_hidden_ValidationType = b.ValidationType x.xxx_hidden_ValidationRegex = b.ValidationRegex return m0 } var File_payload_generator_proto protoreflect.FileDescriptor const file_payload_generator_proto_rawDesc = "" + "\n" + "\x17payload_generator.proto\x12\rtsunami.proto\x1a\x1egoogle/protobuf/wrappers.proto\"\xb7\x06\n" + "\x16PayloadGeneratorConfig\x12f\n" + "\x12vulnerability_type\x18\x02 \x01(\x0e27.tsunami.proto.PayloadGeneratorConfig.VulnerabilityTypeR\x11vulnerabilityType\x12~\n" + "\x1ainterpretation_environment\x18\x03 \x01(\x0e2?.tsunami.proto.PayloadGeneratorConfig.InterpretationEnvironmentR\x19interpretationEnvironment\x12o\n" + "\x15execution_environment\x18\x04 \x01(\x0e2:.tsunami.proto.PayloadGeneratorConfig.ExecutionEnvironmentR\x14executionEnvironment\"\x97\x01\n" + "\x11VulnerabilityType\x12\"\n" + "\x1eVULNERABILITY_TYPE_UNSPECIFIED\x10\x00\x12\x12\n" + "\x0eREFLECTIVE_RCE\x10\x01\x12\r\n" + "\tBLIND_RCE\x10\x02\x12\b\n" + "\x04SSRF\x10\x03\x12\x18\n" + "\x14ARBITRARY_FILE_WRITE\x10\x04\x12\x17\n" + "\x13BLIND_RCE_FILE_READ\x10\x05\"\xb7\x01\n" + "\x19InterpretationEnvironment\x12*\n" + "&INTERPRETATION_ENVIRONMENT_UNSPECIFIED\x10\x00\x12\x0f\n" + "\vLINUX_SHELL\x10\x01\x12\b\n" + "\x04JAVA\x10\x02\x12\a\n" + "\x03PHP\x10\x03\x12\x16\n" + "\x12INTERPRETATION_ANY\x10\x04\x12\x16\n" + "\x12LINUX_ROOT_CRONTAB\x10\x05\x12\x11\n" + "\rWINDOWS_SHELL\x10\x06\x12\a\n" + "\x03JSP\x10\a\"p\n" + "\x14ExecutionEnvironment\x12%\n" + "!EXECUTION_ENVIRONMENT_UNSPECIFIED\x10\x00\x12#\n" + "\x1fEXEC_INTERPRETATION_ENVIRONMENT\x10\x01\x12\f\n" + "\bEXEC_ANY\x10\x02\"E\n" + "\x11PayloadAttributes\x120\n" + "\x14uses_callback_server\x18\x01 \x01(\bR\x12usesCallbackServer\"N\n" + "\x0ePayloadLibrary\x12<\n" + "\bpayloads\x18\x01 \x03(\v2 .tsunami.proto.PayloadDefinitionR\bpayloads\"\xc9\x05\n" + "\x11PayloadDefinition\x120\n" + "\x04name\x18\x01 \x01(\v2\x1c.google.protobuf.StringValueR\x04name\x12~\n" + "\x1ainterpretation_environment\x18\x02 \x01(\x0e2?.tsunami.proto.PayloadGeneratorConfig.InterpretationEnvironmentR\x19interpretationEnvironment\x12o\n" + "\x15execution_environment\x18\x03 \x01(\x0e2:.tsunami.proto.PayloadGeneratorConfig.ExecutionEnvironmentR\x14executionEnvironment\x12f\n" + "\x12vulnerability_type\x18\x04 \x03(\x0e27.tsunami.proto.PayloadGeneratorConfig.VulnerabilityTypeR\x11vulnerabilityType\x12L\n" + "\x14uses_callback_server\x18\x05 \x01(\v2\x1a.google.protobuf.BoolValueR\x12usesCallbackServer\x12C\n" + "\x0epayload_string\x18\x06 \x01(\v2\x1c.google.protobuf.StringValueR\rpayloadString\x12M\n" + "\x0fvalidation_type\x18\a \x01(\x0e2$.tsunami.proto.PayloadValidationTypeR\x0evalidationType\x12G\n" + "\x10validation_regex\x18\b \x01(\v2\x1c.google.protobuf.StringValueR\x0fvalidationRegex*N\n" + "\x15PayloadValidationType\x12\x1f\n" + "\x1bVALIDATION_TYPE_UNSPECIFIED\x10\x00\x12\x14\n" + "\x10VALIDATION_REGEX\x10\x01B\x84\x01\n" + "\x18com.google.tsunami.protoB\x16PayloadGeneratorProtosP\x01ZNgithub.com/google/tsunami-security-scanner/proto/go/payload_generator_go_protob\x06proto3" var file_payload_generator_proto_enumTypes = make([]protoimpl.EnumInfo, 4) var file_payload_generator_proto_msgTypes = make([]protoimpl.MessageInfo, 4) var file_payload_generator_proto_goTypes = []any{ (PayloadValidationType)(0), // 0: tsunami.proto.PayloadValidationType (PayloadGeneratorConfig_VulnerabilityType)(0), // 1: tsunami.proto.PayloadGeneratorConfig.VulnerabilityType (PayloadGeneratorConfig_InterpretationEnvironment)(0), // 2: tsunami.proto.PayloadGeneratorConfig.InterpretationEnvironment (PayloadGeneratorConfig_ExecutionEnvironment)(0), // 3: tsunami.proto.PayloadGeneratorConfig.ExecutionEnvironment (*PayloadGeneratorConfig)(nil), // 4: tsunami.proto.PayloadGeneratorConfig (*PayloadAttributes)(nil), // 5: tsunami.proto.PayloadAttributes (*PayloadLibrary)(nil), // 6: tsunami.proto.PayloadLibrary (*PayloadDefinition)(nil), // 7: tsunami.proto.PayloadDefinition (*wrapperspb.StringValue)(nil), // 8: google.protobuf.StringValue (*wrapperspb.BoolValue)(nil), // 9: google.protobuf.BoolValue } var file_payload_generator_proto_depIdxs = []int32{ 1, // 0: tsunami.proto.PayloadGeneratorConfig.vulnerability_type:type_name -> tsunami.proto.PayloadGeneratorConfig.VulnerabilityType 2, // 1: tsunami.proto.PayloadGeneratorConfig.interpretation_environment:type_name -> tsunami.proto.PayloadGeneratorConfig.InterpretationEnvironment 3, // 2: tsunami.proto.PayloadGeneratorConfig.execution_environment:type_name -> tsunami.proto.PayloadGeneratorConfig.ExecutionEnvironment 7, // 3: tsunami.proto.PayloadLibrary.payloads:type_name -> tsunami.proto.PayloadDefinition 8, // 4: tsunami.proto.PayloadDefinition.name:type_name -> google.protobuf.StringValue 2, // 5: tsunami.proto.PayloadDefinition.interpretation_environment:type_name -> tsunami.proto.PayloadGeneratorConfig.InterpretationEnvironment 3, // 6: tsunami.proto.PayloadDefinition.execution_environment:type_name -> tsunami.proto.PayloadGeneratorConfig.ExecutionEnvironment 1, // 7: tsunami.proto.PayloadDefinition.vulnerability_type:type_name -> tsunami.proto.PayloadGeneratorConfig.VulnerabilityType 9, // 8: tsunami.proto.PayloadDefinition.uses_callback_server:type_name -> google.protobuf.BoolValue 8, // 9: tsunami.proto.PayloadDefinition.payload_string:type_name -> google.protobuf.StringValue 0, // 10: tsunami.proto.PayloadDefinition.validation_type:type_name -> tsunami.proto.PayloadValidationType 8, // 11: tsunami.proto.PayloadDefinition.validation_regex:type_name -> google.protobuf.StringValue 12, // [12:12] is the sub-list for method output_type 12, // [12:12] is the sub-list for method input_type 12, // [12:12] is the sub-list for extension type_name 12, // [12:12] is the sub-list for extension extendee 0, // [0:12] is the sub-list for field type_name } func init() { file_payload_generator_proto_init() } func file_payload_generator_proto_init() { if File_payload_generator_proto != nil { return } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_payload_generator_proto_rawDesc), len(file_payload_generator_proto_rawDesc)), NumEnums: 4, NumMessages: 4, NumExtensions: 0, NumServices: 0, }, GoTypes: file_payload_generator_proto_goTypes, DependencyIndexes: file_payload_generator_proto_depIdxs, EnumInfos: file_payload_generator_proto_enumTypes, MessageInfos: file_payload_generator_proto_msgTypes, }.Build() File_payload_generator_proto = out.File file_payload_generator_proto_goTypes = nil file_payload_generator_proto_depIdxs = nil } ================================================ FILE: proto/go/plugin_representation_go_proto/plugin_representation.pb.go ================================================ // // Copyright 2020 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Representation of a tsunami plugin definition passed between language // servers. // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.11 // protoc v3.21.12 // source: plugin_representation.proto package plugin_representation_go_proto import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" unsafe "unsafe" ) const ( // Verify that this generated code is sufficiently up-to-date. _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) // Verify that runtime/protoimpl is sufficiently up-to-date. _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) type PluginInfo_PluginType int32 const ( // Plugin is an unspecified type. PluginInfo_PLUGIN_TYPE_UNSPECIFIED PluginInfo_PluginType = 0 // Plugin is a port scanner. PluginInfo_PORT_SCAN PluginInfo_PluginType = 1 // Plugin is a service fingerprinter. PluginInfo_SERVICE_FINGERPRINT PluginInfo_PluginType = 2 // Plugin is a vulnerability detector. PluginInfo_VULN_DETECTION PluginInfo_PluginType = 3 ) // Enum value maps for PluginInfo_PluginType. var ( PluginInfo_PluginType_name = map[int32]string{ 0: "PLUGIN_TYPE_UNSPECIFIED", 1: "PORT_SCAN", 2: "SERVICE_FINGERPRINT", 3: "VULN_DETECTION", } PluginInfo_PluginType_value = map[string]int32{ "PLUGIN_TYPE_UNSPECIFIED": 0, "PORT_SCAN": 1, "SERVICE_FINGERPRINT": 2, "VULN_DETECTION": 3, } ) func (x PluginInfo_PluginType) Enum() *PluginInfo_PluginType { p := new(PluginInfo_PluginType) *p = x return p } func (x PluginInfo_PluginType) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (PluginInfo_PluginType) Descriptor() protoreflect.EnumDescriptor { return file_plugin_representation_proto_enumTypes[0].Descriptor() } func (PluginInfo_PluginType) Type() protoreflect.EnumType { return &file_plugin_representation_proto_enumTypes[0] } func (x PluginInfo_PluginType) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } // Represents a PluginDefinition placeholder. type PluginDefinition struct { state protoimpl.MessageState `protogen:"opaque.v1"` xxx_hidden_Info *PluginInfo `protobuf:"bytes,1,opt,name=info,proto3"` xxx_hidden_TargetServiceName *TargetServiceName `protobuf:"bytes,2,opt,name=target_service_name,json=targetServiceName,proto3"` xxx_hidden_TargetSoftware *TargetSoftware `protobuf:"bytes,3,opt,name=target_software,json=targetSoftware,proto3"` xxx_hidden_ForWebService bool `protobuf:"varint,4,opt,name=for_web_service,json=forWebService,proto3"` xxx_hidden_TargetOperatingSystemClass *TargetOperatingSystemClass `protobuf:"bytes,5,opt,name=target_operating_system_class,json=targetOperatingSystemClass,proto3"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *PluginDefinition) Reset() { *x = PluginDefinition{} mi := &file_plugin_representation_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *PluginDefinition) String() string { return protoimpl.X.MessageStringOf(x) } func (*PluginDefinition) ProtoMessage() {} func (x *PluginDefinition) ProtoReflect() protoreflect.Message { mi := &file_plugin_representation_proto_msgTypes[0] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } func (x *PluginDefinition) GetInfo() *PluginInfo { if x != nil { return x.xxx_hidden_Info } return nil } func (x *PluginDefinition) GetTargetServiceName() *TargetServiceName { if x != nil { return x.xxx_hidden_TargetServiceName } return nil } func (x *PluginDefinition) GetTargetSoftware() *TargetSoftware { if x != nil { return x.xxx_hidden_TargetSoftware } return nil } func (x *PluginDefinition) GetForWebService() bool { if x != nil { return x.xxx_hidden_ForWebService } return false } func (x *PluginDefinition) GetTargetOperatingSystemClass() *TargetOperatingSystemClass { if x != nil { return x.xxx_hidden_TargetOperatingSystemClass } return nil } func (x *PluginDefinition) SetInfo(v *PluginInfo) { x.xxx_hidden_Info = v } func (x *PluginDefinition) SetTargetServiceName(v *TargetServiceName) { x.xxx_hidden_TargetServiceName = v } func (x *PluginDefinition) SetTargetSoftware(v *TargetSoftware) { x.xxx_hidden_TargetSoftware = v } func (x *PluginDefinition) SetForWebService(v bool) { x.xxx_hidden_ForWebService = v } func (x *PluginDefinition) SetTargetOperatingSystemClass(v *TargetOperatingSystemClass) { x.xxx_hidden_TargetOperatingSystemClass = v } func (x *PluginDefinition) HasInfo() bool { if x == nil { return false } return x.xxx_hidden_Info != nil } func (x *PluginDefinition) HasTargetServiceName() bool { if x == nil { return false } return x.xxx_hidden_TargetServiceName != nil } func (x *PluginDefinition) HasTargetSoftware() bool { if x == nil { return false } return x.xxx_hidden_TargetSoftware != nil } func (x *PluginDefinition) HasTargetOperatingSystemClass() bool { if x == nil { return false } return x.xxx_hidden_TargetOperatingSystemClass != nil } func (x *PluginDefinition) ClearInfo() { x.xxx_hidden_Info = nil } func (x *PluginDefinition) ClearTargetServiceName() { x.xxx_hidden_TargetServiceName = nil } func (x *PluginDefinition) ClearTargetSoftware() { x.xxx_hidden_TargetSoftware = nil } func (x *PluginDefinition) ClearTargetOperatingSystemClass() { x.xxx_hidden_TargetOperatingSystemClass = nil } type PluginDefinition_builder struct { _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. // PluginInfo of this definition. Info *PluginInfo // The name of the target service. TargetServiceName *TargetServiceName // The name of the target software. TargetSoftware *TargetSoftware // If the definition is for a web service or not. ForWebService bool // If the definition is for a specific operating system or not. // Note: this filter is executed within an AND condition with the other // filters. E.g. if target_service_name.value is "http" and // target_operating_system.osclass.family is "Linux" then the plugin will only // match if the service is http and the operating system is Linux. TargetOperatingSystemClass *TargetOperatingSystemClass } func (b0 PluginDefinition_builder) Build() *PluginDefinition { m0 := &PluginDefinition{} b, x := &b0, m0 _, _ = b, x x.xxx_hidden_Info = b.Info x.xxx_hidden_TargetServiceName = b.TargetServiceName x.xxx_hidden_TargetSoftware = b.TargetSoftware x.xxx_hidden_ForWebService = b.ForWebService x.xxx_hidden_TargetOperatingSystemClass = b.TargetOperatingSystemClass return m0 } // Represents a PluginInfo annotation placeholder used by the // PluginDefinition proto above. type PluginInfo struct { state protoimpl.MessageState `protogen:"opaque.v1"` xxx_hidden_Type PluginInfo_PluginType `protobuf:"varint,1,opt,name=type,proto3,enum=tsunami.proto.PluginInfo_PluginType"` xxx_hidden_Name string `protobuf:"bytes,2,opt,name=name,proto3"` xxx_hidden_Version string `protobuf:"bytes,3,opt,name=version,proto3"` xxx_hidden_Description string `protobuf:"bytes,4,opt,name=description,proto3"` xxx_hidden_Author string `protobuf:"bytes,5,opt,name=author,proto3"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *PluginInfo) Reset() { *x = PluginInfo{} mi := &file_plugin_representation_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *PluginInfo) String() string { return protoimpl.X.MessageStringOf(x) } func (*PluginInfo) ProtoMessage() {} func (x *PluginInfo) ProtoReflect() protoreflect.Message { mi := &file_plugin_representation_proto_msgTypes[1] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } func (x *PluginInfo) GetType() PluginInfo_PluginType { if x != nil { return x.xxx_hidden_Type } return PluginInfo_PLUGIN_TYPE_UNSPECIFIED } func (x *PluginInfo) GetName() string { if x != nil { return x.xxx_hidden_Name } return "" } func (x *PluginInfo) GetVersion() string { if x != nil { return x.xxx_hidden_Version } return "" } func (x *PluginInfo) GetDescription() string { if x != nil { return x.xxx_hidden_Description } return "" } func (x *PluginInfo) GetAuthor() string { if x != nil { return x.xxx_hidden_Author } return "" } func (x *PluginInfo) SetType(v PluginInfo_PluginType) { x.xxx_hidden_Type = v } func (x *PluginInfo) SetName(v string) { x.xxx_hidden_Name = v } func (x *PluginInfo) SetVersion(v string) { x.xxx_hidden_Version = v } func (x *PluginInfo) SetDescription(v string) { x.xxx_hidden_Description = v } func (x *PluginInfo) SetAuthor(v string) { x.xxx_hidden_Author = v } type PluginInfo_builder struct { _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. // Type of plugin. Type PluginInfo_PluginType // Name of the plugin. Name string // Version of the plugin Version string // Description of the plugin. Description string // Author of the plugin. Author string } func (b0 PluginInfo_builder) Build() *PluginInfo { m0 := &PluginInfo{} b, x := &b0, m0 _, _ = b, x x.xxx_hidden_Type = b.Type x.xxx_hidden_Name = b.Name x.xxx_hidden_Version = b.Version x.xxx_hidden_Description = b.Description x.xxx_hidden_Author = b.Author return m0 } // Represents a ForServiceName annotation placeholder used by the // PluginDefinition proto above. type TargetServiceName struct { state protoimpl.MessageState `protogen:"opaque.v1"` xxx_hidden_Value []string `protobuf:"bytes,1,rep,name=value,proto3"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *TargetServiceName) Reset() { *x = TargetServiceName{} mi := &file_plugin_representation_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *TargetServiceName) String() string { return protoimpl.X.MessageStringOf(x) } func (*TargetServiceName) ProtoMessage() {} func (x *TargetServiceName) ProtoReflect() protoreflect.Message { mi := &file_plugin_representation_proto_msgTypes[2] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } func (x *TargetServiceName) GetValue() []string { if x != nil { return x.xxx_hidden_Value } return nil } func (x *TargetServiceName) SetValue(v []string) { x.xxx_hidden_Value = v } type TargetServiceName_builder struct { _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. // The value of the name of the target. Value []string } func (b0 TargetServiceName_builder) Build() *TargetServiceName { m0 := &TargetServiceName{} b, x := &b0, m0 _, _ = b, x x.xxx_hidden_Value = b.Value return m0 } // Represents a ForSoftware annotation placeholder used by the // PluginDefinition proto above. type TargetSoftware struct { state protoimpl.MessageState `protogen:"opaque.v1"` xxx_hidden_Name string `protobuf:"bytes,1,opt,name=name,proto3"` xxx_hidden_Value []string `protobuf:"bytes,2,rep,name=value,proto3"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *TargetSoftware) Reset() { *x = TargetSoftware{} mi := &file_plugin_representation_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *TargetSoftware) String() string { return protoimpl.X.MessageStringOf(x) } func (*TargetSoftware) ProtoMessage() {} func (x *TargetSoftware) ProtoReflect() protoreflect.Message { mi := &file_plugin_representation_proto_msgTypes[3] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } func (x *TargetSoftware) GetName() string { if x != nil { return x.xxx_hidden_Name } return "" } func (x *TargetSoftware) GetValue() []string { if x != nil { return x.xxx_hidden_Value } return nil } func (x *TargetSoftware) SetName(v string) { x.xxx_hidden_Name = v } func (x *TargetSoftware) SetValue(v []string) { x.xxx_hidden_Value = v } type TargetSoftware_builder struct { _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. // The name of the target software, case insensitive. Name string // Array of versions and version ranges of the target software. Value []string } func (b0 TargetSoftware_builder) Build() *TargetSoftware { m0 := &TargetSoftware{} b, x := &b0, m0 _, _ = b, x x.xxx_hidden_Name = b.Name x.xxx_hidden_Value = b.Value return m0 } // Represents a ForOperatingSystem annotation placeholder used by the // PluginDefinition proto above. These values are coming directly from the // port scanner's output (e.g. nmap). type TargetOperatingSystemClass struct { state protoimpl.MessageState `protogen:"opaque.v1"` xxx_hidden_Vendor []string `protobuf:"bytes,1,rep,name=vendor,proto3"` xxx_hidden_OsFamily []string `protobuf:"bytes,2,rep,name=os_family,json=osFamily,proto3"` xxx_hidden_MinAccuracy uint32 `protobuf:"varint,3,opt,name=min_accuracy,json=minAccuracy,proto3"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *TargetOperatingSystemClass) Reset() { *x = TargetOperatingSystemClass{} mi := &file_plugin_representation_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *TargetOperatingSystemClass) String() string { return protoimpl.X.MessageStringOf(x) } func (*TargetOperatingSystemClass) ProtoMessage() {} func (x *TargetOperatingSystemClass) ProtoReflect() protoreflect.Message { mi := &file_plugin_representation_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } func (x *TargetOperatingSystemClass) GetVendor() []string { if x != nil { return x.xxx_hidden_Vendor } return nil } func (x *TargetOperatingSystemClass) GetOsFamily() []string { if x != nil { return x.xxx_hidden_OsFamily } return nil } func (x *TargetOperatingSystemClass) GetMinAccuracy() uint32 { if x != nil { return x.xxx_hidden_MinAccuracy } return 0 } func (x *TargetOperatingSystemClass) SetVendor(v []string) { x.xxx_hidden_Vendor = v } func (x *TargetOperatingSystemClass) SetOsFamily(v []string) { x.xxx_hidden_OsFamily = v } func (x *TargetOperatingSystemClass) SetMinAccuracy(v uint32) { x.xxx_hidden_MinAccuracy = v } type TargetOperatingSystemClass_builder struct { _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. // The vendor of the target operating system, e.g. "Microsoft" Vendor []string // The family of the target operating system, e.g. "Windows" OsFamily []string // The minimum accuracy of the target operating system, e.g. 90 MinAccuracy uint32 } func (b0 TargetOperatingSystemClass_builder) Build() *TargetOperatingSystemClass { m0 := &TargetOperatingSystemClass{} b, x := &b0, m0 _, _ = b, x x.xxx_hidden_Vendor = b.Vendor x.xxx_hidden_OsFamily = b.OsFamily x.xxx_hidden_MinAccuracy = b.MinAccuracy return m0 } var File_plugin_representation_proto protoreflect.FileDescriptor const file_plugin_representation_proto_rawDesc = "" + "\n" + "\x1bplugin_representation.proto\x12\rtsunami.proto\"\xf1\x02\n" + "\x10PluginDefinition\x12-\n" + "\x04info\x18\x01 \x01(\v2\x19.tsunami.proto.PluginInfoR\x04info\x12P\n" + "\x13target_service_name\x18\x02 \x01(\v2 .tsunami.proto.TargetServiceNameR\x11targetServiceName\x12F\n" + "\x0ftarget_software\x18\x03 \x01(\v2\x1d.tsunami.proto.TargetSoftwareR\x0etargetSoftware\x12&\n" + "\x0ffor_web_service\x18\x04 \x01(\bR\rforWebService\x12l\n" + "\x1dtarget_operating_system_class\x18\x05 \x01(\v2).tsunami.proto.TargetOperatingSystemClassR\x1atargetOperatingSystemClass\"\x95\x02\n" + "\n" + "PluginInfo\x128\n" + "\x04type\x18\x01 \x01(\x0e2$.tsunami.proto.PluginInfo.PluginTypeR\x04type\x12\x12\n" + "\x04name\x18\x02 \x01(\tR\x04name\x12\x18\n" + "\aversion\x18\x03 \x01(\tR\aversion\x12 \n" + "\vdescription\x18\x04 \x01(\tR\vdescription\x12\x16\n" + "\x06author\x18\x05 \x01(\tR\x06author\"e\n" + "\n" + "PluginType\x12\x1b\n" + "\x17PLUGIN_TYPE_UNSPECIFIED\x10\x00\x12\r\n" + "\tPORT_SCAN\x10\x01\x12\x17\n" + "\x13SERVICE_FINGERPRINT\x10\x02\x12\x12\n" + "\x0eVULN_DETECTION\x10\x03\")\n" + "\x11TargetServiceName\x12\x14\n" + "\x05value\x18\x01 \x03(\tR\x05value\":\n" + "\x0eTargetSoftware\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x14\n" + "\x05value\x18\x02 \x03(\tR\x05value\"t\n" + "\x1aTargetOperatingSystemClass\x12\x16\n" + "\x06vendor\x18\x01 \x03(\tR\x06vendor\x12\x1b\n" + "\tos_family\x18\x02 \x03(\tR\bosFamily\x12!\n" + "\fmin_accuracy\x18\x03 \x01(\rR\vminAccuracyB\x8c\x01\n" + "\x18com.google.tsunami.protoB\x1aPluginRepresentationProtosP\x01ZRgithub.com/google/tsunami-security-scanner/proto/go/plugin_representation_go_protob\x06proto3" var file_plugin_representation_proto_enumTypes = make([]protoimpl.EnumInfo, 1) var file_plugin_representation_proto_msgTypes = make([]protoimpl.MessageInfo, 5) var file_plugin_representation_proto_goTypes = []any{ (PluginInfo_PluginType)(0), // 0: tsunami.proto.PluginInfo.PluginType (*PluginDefinition)(nil), // 1: tsunami.proto.PluginDefinition (*PluginInfo)(nil), // 2: tsunami.proto.PluginInfo (*TargetServiceName)(nil), // 3: tsunami.proto.TargetServiceName (*TargetSoftware)(nil), // 4: tsunami.proto.TargetSoftware (*TargetOperatingSystemClass)(nil), // 5: tsunami.proto.TargetOperatingSystemClass } var file_plugin_representation_proto_depIdxs = []int32{ 2, // 0: tsunami.proto.PluginDefinition.info:type_name -> tsunami.proto.PluginInfo 3, // 1: tsunami.proto.PluginDefinition.target_service_name:type_name -> tsunami.proto.TargetServiceName 4, // 2: tsunami.proto.PluginDefinition.target_software:type_name -> tsunami.proto.TargetSoftware 5, // 3: tsunami.proto.PluginDefinition.target_operating_system_class:type_name -> tsunami.proto.TargetOperatingSystemClass 0, // 4: tsunami.proto.PluginInfo.type:type_name -> tsunami.proto.PluginInfo.PluginType 5, // [5:5] is the sub-list for method output_type 5, // [5:5] is the sub-list for method input_type 5, // [5:5] is the sub-list for extension type_name 5, // [5:5] is the sub-list for extension extendee 0, // [0:5] is the sub-list for field type_name } func init() { file_plugin_representation_proto_init() } func file_plugin_representation_proto_init() { if File_plugin_representation_proto != nil { return } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_plugin_representation_proto_rawDesc), len(file_plugin_representation_proto_rawDesc)), NumEnums: 1, NumMessages: 5, NumExtensions: 0, NumServices: 0, }, GoTypes: file_plugin_representation_proto_goTypes, DependencyIndexes: file_plugin_representation_proto_depIdxs, EnumInfos: file_plugin_representation_proto_enumTypes, MessageInfos: file_plugin_representation_proto_msgTypes, }.Build() File_plugin_representation_proto = out.File file_plugin_representation_proto_goTypes = nil file_plugin_representation_proto_depIdxs = nil } ================================================ FILE: proto/go/plugin_service_go_proto/plugin_service.pb.go ================================================ // // Copyright 2022 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Model for the plugin RPC service protocol. // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.11 // protoc v3.21.12 // source: plugin_service.proto package plugin_service_go_proto import ( detection_go_proto "github.com/google/tsunami-security-scanner/proto/go/detection_go_proto" network_service_go_proto "github.com/google/tsunami-security-scanner/proto/go/network_service_go_proto" plugin_representation_go_proto "github.com/google/tsunami-security-scanner/proto/go/plugin_representation_go_proto" reconnaissance_go_proto "github.com/google/tsunami-security-scanner/proto/go/reconnaissance_go_proto" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" unsafe "unsafe" ) const ( // Verify that this generated code is sufficiently up-to-date. _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) // Verify that runtime/protoimpl is sufficiently up-to-date. _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) // Represents a run request with all matched plugins that will need to run // as well as the target to run against. type RunRequest struct { state protoimpl.MessageState `protogen:"opaque.v1"` xxx_hidden_Target *reconnaissance_go_proto.TargetInfo `protobuf:"bytes,1,opt,name=target,proto3"` xxx_hidden_Plugins *[]*MatchedPlugin `protobuf:"bytes,2,rep,name=plugins,proto3"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *RunRequest) Reset() { *x = RunRequest{} mi := &file_plugin_service_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *RunRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*RunRequest) ProtoMessage() {} func (x *RunRequest) ProtoReflect() protoreflect.Message { mi := &file_plugin_service_proto_msgTypes[0] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } func (x *RunRequest) GetTarget() *reconnaissance_go_proto.TargetInfo { if x != nil { return x.xxx_hidden_Target } return nil } func (x *RunRequest) GetPlugins() []*MatchedPlugin { if x != nil { if x.xxx_hidden_Plugins != nil { return *x.xxx_hidden_Plugins } } return nil } func (x *RunRequest) SetTarget(v *reconnaissance_go_proto.TargetInfo) { x.xxx_hidden_Target = v } func (x *RunRequest) SetPlugins(v []*MatchedPlugin) { x.xxx_hidden_Plugins = &v } func (x *RunRequest) HasTarget() bool { if x == nil { return false } return x.xxx_hidden_Target != nil } func (x *RunRequest) ClearTarget() { x.xxx_hidden_Target = nil } type RunRequest_builder struct { _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. // Target of the plugins. Target *reconnaissance_go_proto.TargetInfo // All matched plugins that will need to run. Plugins []*MatchedPlugin } func (b0 RunRequest_builder) Build() *RunRequest { m0 := &RunRequest{} b, x := &b0, m0 _, _ = b, x x.xxx_hidden_Target = b.Target x.xxx_hidden_Plugins = &b.Plugins return m0 } // Compact representation of RunRequest. type RunCompactRequest struct { state protoimpl.MessageState `protogen:"opaque.v1"` xxx_hidden_Target *reconnaissance_go_proto.TargetInfo `protobuf:"bytes,1,opt,name=target,proto3"` xxx_hidden_Services *[]*network_service_go_proto.NetworkService `protobuf:"bytes,2,rep,name=services,proto3"` xxx_hidden_Plugins *[]*plugin_representation_go_proto.PluginDefinition `protobuf:"bytes,3,rep,name=plugins,proto3"` xxx_hidden_ScanTargets *[]*RunCompactRequest_PluginNetworkServiceTarget `protobuf:"bytes,4,rep,name=scan_targets,json=scanTargets,proto3"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *RunCompactRequest) Reset() { *x = RunCompactRequest{} mi := &file_plugin_service_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *RunCompactRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*RunCompactRequest) ProtoMessage() {} func (x *RunCompactRequest) ProtoReflect() protoreflect.Message { mi := &file_plugin_service_proto_msgTypes[1] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } func (x *RunCompactRequest) GetTarget() *reconnaissance_go_proto.TargetInfo { if x != nil { return x.xxx_hidden_Target } return nil } func (x *RunCompactRequest) GetServices() []*network_service_go_proto.NetworkService { if x != nil { if x.xxx_hidden_Services != nil { return *x.xxx_hidden_Services } } return nil } func (x *RunCompactRequest) GetPlugins() []*plugin_representation_go_proto.PluginDefinition { if x != nil { if x.xxx_hidden_Plugins != nil { return *x.xxx_hidden_Plugins } } return nil } func (x *RunCompactRequest) GetScanTargets() []*RunCompactRequest_PluginNetworkServiceTarget { if x != nil { if x.xxx_hidden_ScanTargets != nil { return *x.xxx_hidden_ScanTargets } } return nil } func (x *RunCompactRequest) SetTarget(v *reconnaissance_go_proto.TargetInfo) { x.xxx_hidden_Target = v } func (x *RunCompactRequest) SetServices(v []*network_service_go_proto.NetworkService) { x.xxx_hidden_Services = &v } func (x *RunCompactRequest) SetPlugins(v []*plugin_representation_go_proto.PluginDefinition) { x.xxx_hidden_Plugins = &v } func (x *RunCompactRequest) SetScanTargets(v []*RunCompactRequest_PluginNetworkServiceTarget) { x.xxx_hidden_ScanTargets = &v } func (x *RunCompactRequest) HasTarget() bool { if x == nil { return false } return x.xxx_hidden_Target != nil } func (x *RunCompactRequest) ClearTarget() { x.xxx_hidden_Target = nil } type RunCompactRequest_builder struct { _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. // Target of the plugins. Target *reconnaissance_go_proto.TargetInfo // All network services that are targeted by some of the plugins. Services []*network_service_go_proto.NetworkService // All plugins that should be executed during the run. Plugins []*plugin_representation_go_proto.PluginDefinition // The concrete map of plugin/network service pairs that should be scanned. ScanTargets []*RunCompactRequest_PluginNetworkServiceTarget } func (b0 RunCompactRequest_builder) Build() *RunCompactRequest { m0 := &RunCompactRequest{} b, x := &b0, m0 _, _ = b, x x.xxx_hidden_Target = b.Target x.xxx_hidden_Services = &b.Services x.xxx_hidden_Plugins = &b.Plugins x.xxx_hidden_ScanTargets = &b.ScanTargets return m0 } // Represents the plugin needed to run by the language-specific server // as well as all the matched network services for the plugin. type MatchedPlugin struct { state protoimpl.MessageState `protogen:"opaque.v1"` xxx_hidden_Services *[]*network_service_go_proto.NetworkService `protobuf:"bytes,1,rep,name=services,proto3"` xxx_hidden_Plugin *plugin_representation_go_proto.PluginDefinition `protobuf:"bytes,2,opt,name=plugin,proto3"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *MatchedPlugin) Reset() { *x = MatchedPlugin{} mi := &file_plugin_service_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *MatchedPlugin) String() string { return protoimpl.X.MessageStringOf(x) } func (*MatchedPlugin) ProtoMessage() {} func (x *MatchedPlugin) ProtoReflect() protoreflect.Message { mi := &file_plugin_service_proto_msgTypes[2] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } func (x *MatchedPlugin) GetServices() []*network_service_go_proto.NetworkService { if x != nil { if x.xxx_hidden_Services != nil { return *x.xxx_hidden_Services } } return nil } func (x *MatchedPlugin) GetPlugin() *plugin_representation_go_proto.PluginDefinition { if x != nil { return x.xxx_hidden_Plugin } return nil } func (x *MatchedPlugin) SetServices(v []*network_service_go_proto.NetworkService) { x.xxx_hidden_Services = &v } func (x *MatchedPlugin) SetPlugin(v *plugin_representation_go_proto.PluginDefinition) { x.xxx_hidden_Plugin = v } func (x *MatchedPlugin) HasPlugin() bool { if x == nil { return false } return x.xxx_hidden_Plugin != nil } func (x *MatchedPlugin) ClearPlugin() { x.xxx_hidden_Plugin = nil } type MatchedPlugin_builder struct { _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. // All matched network services from the reconnaissance report. Services []*network_service_go_proto.NetworkService // Plugin to run. Plugin *plugin_representation_go_proto.PluginDefinition } func (b0 MatchedPlugin_builder) Build() *MatchedPlugin { m0 := &MatchedPlugin{} b, x := &b0, m0 _, _ = b, x x.xxx_hidden_Services = &b.Services x.xxx_hidden_Plugin = b.Plugin return m0 } // Represents a run response with the only field being all DetectionReports // generated by the language-specific server. type RunResponse struct { state protoimpl.MessageState `protogen:"opaque.v1"` xxx_hidden_Reports *detection_go_proto.DetectionReportList `protobuf:"bytes,1,opt,name=reports,proto3"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *RunResponse) Reset() { *x = RunResponse{} mi := &file_plugin_service_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *RunResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*RunResponse) ProtoMessage() {} func (x *RunResponse) ProtoReflect() protoreflect.Message { mi := &file_plugin_service_proto_msgTypes[3] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } func (x *RunResponse) GetReports() *detection_go_proto.DetectionReportList { if x != nil { return x.xxx_hidden_Reports } return nil } func (x *RunResponse) SetReports(v *detection_go_proto.DetectionReportList) { x.xxx_hidden_Reports = v } func (x *RunResponse) HasReports() bool { if x == nil { return false } return x.xxx_hidden_Reports != nil } func (x *RunResponse) ClearReports() { x.xxx_hidden_Reports = nil } type RunResponse_builder struct { _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. Reports *detection_go_proto.DetectionReportList } func (b0 RunResponse_builder) Build() *RunResponse { m0 := &RunResponse{} b, x := &b0, m0 _, _ = b, x x.xxx_hidden_Reports = b.Reports return m0 } // Represents a request to list all plugins from the requested server. type ListPluginsRequest struct { state protoimpl.MessageState `protogen:"opaque.v1"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *ListPluginsRequest) Reset() { *x = ListPluginsRequest{} mi := &file_plugin_service_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *ListPluginsRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*ListPluginsRequest) ProtoMessage() {} func (x *ListPluginsRequest) ProtoReflect() protoreflect.Message { mi := &file_plugin_service_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } type ListPluginsRequest_builder struct { _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. } func (b0 ListPluginsRequest_builder) Build() *ListPluginsRequest { m0 := &ListPluginsRequest{} b, x := &b0, m0 _, _ = b, x return m0 } // Represents a response containing a list of all plugins // from the requested server. type ListPluginsResponse struct { state protoimpl.MessageState `protogen:"opaque.v1"` xxx_hidden_Plugins *[]*plugin_representation_go_proto.PluginDefinition `protobuf:"bytes,1,rep,name=plugins,proto3"` xxx_hidden_WantCompactRunRequest bool `protobuf:"varint,2,opt,name=want_compact_run_request,json=wantCompactRunRequest,proto3"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *ListPluginsResponse) Reset() { *x = ListPluginsResponse{} mi := &file_plugin_service_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *ListPluginsResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*ListPluginsResponse) ProtoMessage() {} func (x *ListPluginsResponse) ProtoReflect() protoreflect.Message { mi := &file_plugin_service_proto_msgTypes[5] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } func (x *ListPluginsResponse) GetPlugins() []*plugin_representation_go_proto.PluginDefinition { if x != nil { if x.xxx_hidden_Plugins != nil { return *x.xxx_hidden_Plugins } } return nil } func (x *ListPluginsResponse) GetWantCompactRunRequest() bool { if x != nil { return x.xxx_hidden_WantCompactRunRequest } return false } func (x *ListPluginsResponse) SetPlugins(v []*plugin_representation_go_proto.PluginDefinition) { x.xxx_hidden_Plugins = &v } func (x *ListPluginsResponse) SetWantCompactRunRequest(v bool) { x.xxx_hidden_WantCompactRunRequest = v } type ListPluginsResponse_builder struct { _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. Plugins []*plugin_representation_go_proto.PluginDefinition // Plugin service can indicate here that it RunRequest should be compact // (compact_targets should be populated instead of MatchedPlugin plugins). WantCompactRunRequest bool } func (b0 ListPluginsResponse_builder) Build() *ListPluginsResponse { m0 := &ListPluginsResponse{} b, x := &b0, m0 _, _ = b, x x.xxx_hidden_Plugins = &b.Plugins x.xxx_hidden_WantCompactRunRequest = b.WantCompactRunRequest return m0 } // Indexes in the following structure point to the services/plugins defined // below. (The order is safe, guaranteed by the proto specification: "The // order of the elements with respect to each other is preserved when parsing, // though the ordering with respect to other fields is lost.") type RunCompactRequest_PluginNetworkServiceTarget struct { state protoimpl.MessageState `protogen:"opaque.v1"` xxx_hidden_PluginIndex uint32 `protobuf:"varint,1,opt,name=plugin_index,json=pluginIndex,proto3"` xxx_hidden_ServiceIndex uint32 `protobuf:"varint,2,opt,name=service_index,json=serviceIndex,proto3"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *RunCompactRequest_PluginNetworkServiceTarget) Reset() { *x = RunCompactRequest_PluginNetworkServiceTarget{} mi := &file_plugin_service_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *RunCompactRequest_PluginNetworkServiceTarget) String() string { return protoimpl.X.MessageStringOf(x) } func (*RunCompactRequest_PluginNetworkServiceTarget) ProtoMessage() {} func (x *RunCompactRequest_PluginNetworkServiceTarget) ProtoReflect() protoreflect.Message { mi := &file_plugin_service_proto_msgTypes[6] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } func (x *RunCompactRequest_PluginNetworkServiceTarget) GetPluginIndex() uint32 { if x != nil { return x.xxx_hidden_PluginIndex } return 0 } func (x *RunCompactRequest_PluginNetworkServiceTarget) GetServiceIndex() uint32 { if x != nil { return x.xxx_hidden_ServiceIndex } return 0 } func (x *RunCompactRequest_PluginNetworkServiceTarget) SetPluginIndex(v uint32) { x.xxx_hidden_PluginIndex = v } func (x *RunCompactRequest_PluginNetworkServiceTarget) SetServiceIndex(v uint32) { x.xxx_hidden_ServiceIndex = v } type RunCompactRequest_PluginNetworkServiceTarget_builder struct { _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. // The index of the plugin to run. PluginIndex uint32 // The index of the network service to run against. ServiceIndex uint32 } func (b0 RunCompactRequest_PluginNetworkServiceTarget_builder) Build() *RunCompactRequest_PluginNetworkServiceTarget { m0 := &RunCompactRequest_PluginNetworkServiceTarget{} b, x := &b0, m0 _, _ = b, x x.xxx_hidden_PluginIndex = b.PluginIndex x.xxx_hidden_ServiceIndex = b.ServiceIndex return m0 } var File_plugin_service_proto protoreflect.FileDescriptor const file_plugin_service_proto_rawDesc = "" + "\n" + "\x14plugin_service.proto\x12\rtsunami.proto\x1a\x0fdetection.proto\x1a\x15network_service.proto\x1a\x1bplugin_representation.proto\x1a\x14reconnaissance.proto\"w\n" + "\n" + "RunRequest\x121\n" + "\x06target\x18\x01 \x01(\v2\x19.tsunami.proto.TargetInfoR\x06target\x126\n" + "\aplugins\x18\x02 \x03(\v2\x1c.tsunami.proto.MatchedPluginR\aplugins\"\x82\x03\n" + "\x11RunCompactRequest\x121\n" + "\x06target\x18\x01 \x01(\v2\x19.tsunami.proto.TargetInfoR\x06target\x129\n" + "\bservices\x18\x02 \x03(\v2\x1d.tsunami.proto.NetworkServiceR\bservices\x129\n" + "\aplugins\x18\x03 \x03(\v2\x1f.tsunami.proto.PluginDefinitionR\aplugins\x12^\n" + "\fscan_targets\x18\x04 \x03(\v2;.tsunami.proto.RunCompactRequest.PluginNetworkServiceTargetR\vscanTargets\x1ad\n" + "\x1aPluginNetworkServiceTarget\x12!\n" + "\fplugin_index\x18\x01 \x01(\rR\vpluginIndex\x12#\n" + "\rservice_index\x18\x02 \x01(\rR\fserviceIndex\"\x83\x01\n" + "\rMatchedPlugin\x129\n" + "\bservices\x18\x01 \x03(\v2\x1d.tsunami.proto.NetworkServiceR\bservices\x127\n" + "\x06plugin\x18\x02 \x01(\v2\x1f.tsunami.proto.PluginDefinitionR\x06plugin\"K\n" + "\vRunResponse\x12<\n" + "\areports\x18\x01 \x01(\v2\".tsunami.proto.DetectionReportListR\areports\"\x14\n" + "\x12ListPluginsRequest\"\x89\x01\n" + "\x13ListPluginsResponse\x129\n" + "\aplugins\x18\x01 \x03(\v2\x1f.tsunami.proto.PluginDefinitionR\aplugins\x127\n" + "\x18want_compact_run_request\x18\x02 \x01(\bR\x15wantCompactRunRequest2\xf5\x01\n" + "\rPluginService\x12>\n" + "\x03Run\x12\x19.tsunami.proto.RunRequest\x1a\x1a.tsunami.proto.RunResponse\"\x00\x12L\n" + "\n" + "RunCompact\x12 .tsunami.proto.RunCompactRequest\x1a\x1a.tsunami.proto.RunResponse\"\x00\x12V\n" + "\vListPlugins\x12!.tsunami.proto.ListPluginsRequest\x1a\".tsunami.proto.ListPluginsResponse\"\x00B~\n" + "\x18com.google.tsunami.protoB\x13PluginServiceProtosP\x01ZKgithub.com/google/tsunami-security-scanner/proto/go/plugin_service_go_protob\x06proto3" var file_plugin_service_proto_msgTypes = make([]protoimpl.MessageInfo, 7) var file_plugin_service_proto_goTypes = []any{ (*RunRequest)(nil), // 0: tsunami.proto.RunRequest (*RunCompactRequest)(nil), // 1: tsunami.proto.RunCompactRequest (*MatchedPlugin)(nil), // 2: tsunami.proto.MatchedPlugin (*RunResponse)(nil), // 3: tsunami.proto.RunResponse (*ListPluginsRequest)(nil), // 4: tsunami.proto.ListPluginsRequest (*ListPluginsResponse)(nil), // 5: tsunami.proto.ListPluginsResponse (*RunCompactRequest_PluginNetworkServiceTarget)(nil), // 6: tsunami.proto.RunCompactRequest.PluginNetworkServiceTarget (*reconnaissance_go_proto.TargetInfo)(nil), // 7: tsunami.proto.TargetInfo (*network_service_go_proto.NetworkService)(nil), // 8: tsunami.proto.NetworkService (*plugin_representation_go_proto.PluginDefinition)(nil), // 9: tsunami.proto.PluginDefinition (*detection_go_proto.DetectionReportList)(nil), // 10: tsunami.proto.DetectionReportList } var file_plugin_service_proto_depIdxs = []int32{ 7, // 0: tsunami.proto.RunRequest.target:type_name -> tsunami.proto.TargetInfo 2, // 1: tsunami.proto.RunRequest.plugins:type_name -> tsunami.proto.MatchedPlugin 7, // 2: tsunami.proto.RunCompactRequest.target:type_name -> tsunami.proto.TargetInfo 8, // 3: tsunami.proto.RunCompactRequest.services:type_name -> tsunami.proto.NetworkService 9, // 4: tsunami.proto.RunCompactRequest.plugins:type_name -> tsunami.proto.PluginDefinition 6, // 5: tsunami.proto.RunCompactRequest.scan_targets:type_name -> tsunami.proto.RunCompactRequest.PluginNetworkServiceTarget 8, // 6: tsunami.proto.MatchedPlugin.services:type_name -> tsunami.proto.NetworkService 9, // 7: tsunami.proto.MatchedPlugin.plugin:type_name -> tsunami.proto.PluginDefinition 10, // 8: tsunami.proto.RunResponse.reports:type_name -> tsunami.proto.DetectionReportList 9, // 9: tsunami.proto.ListPluginsResponse.plugins:type_name -> tsunami.proto.PluginDefinition 0, // 10: tsunami.proto.PluginService.Run:input_type -> tsunami.proto.RunRequest 1, // 11: tsunami.proto.PluginService.RunCompact:input_type -> tsunami.proto.RunCompactRequest 4, // 12: tsunami.proto.PluginService.ListPlugins:input_type -> tsunami.proto.ListPluginsRequest 3, // 13: tsunami.proto.PluginService.Run:output_type -> tsunami.proto.RunResponse 3, // 14: tsunami.proto.PluginService.RunCompact:output_type -> tsunami.proto.RunResponse 5, // 15: tsunami.proto.PluginService.ListPlugins:output_type -> tsunami.proto.ListPluginsResponse 13, // [13:16] is the sub-list for method output_type 10, // [10:13] is the sub-list for method input_type 10, // [10:10] is the sub-list for extension type_name 10, // [10:10] is the sub-list for extension extendee 0, // [0:10] is the sub-list for field type_name } func init() { file_plugin_service_proto_init() } func file_plugin_service_proto_init() { if File_plugin_service_proto != nil { return } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_plugin_service_proto_rawDesc), len(file_plugin_service_proto_rawDesc)), NumEnums: 0, NumMessages: 7, NumExtensions: 0, NumServices: 1, }, GoTypes: file_plugin_service_proto_goTypes, DependencyIndexes: file_plugin_service_proto_depIdxs, MessageInfos: file_plugin_service_proto_msgTypes, }.Build() File_plugin_service_proto = out.File file_plugin_service_proto_goTypes = nil file_plugin_service_proto_depIdxs = nil } ================================================ FILE: proto/go/reconnaissance_go_proto/reconnaissance.pb.go ================================================ // // Copyright 2019 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Data models for all the reconnaissance information gathered by Tsunami. // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.11 // protoc v3.21.12 // source: reconnaissance.proto package reconnaissance_go_proto import ( network_go_proto "github.com/google/tsunami-security-scanner/proto/go/network_go_proto" network_service_go_proto "github.com/google/tsunami-security-scanner/proto/go/network_service_go_proto" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" unsafe "unsafe" ) const ( // Verify that this generated code is sufficiently up-to-date. _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) // Verify that runtime/protoimpl is sufficiently up-to-date. _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) // Detailed information about the scanning target. type TargetInfo struct { state protoimpl.MessageState `protogen:"opaque.v1"` xxx_hidden_NetworkEndpoints *[]*network_go_proto.NetworkEndpoint `protobuf:"bytes,1,rep,name=network_endpoints,json=networkEndpoints,proto3"` xxx_hidden_OperatingSystemClasses *[]*OperatingSystemClass `protobuf:"bytes,2,rep,name=operating_system_classes,json=operatingSystemClasses,proto3"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *TargetInfo) Reset() { *x = TargetInfo{} mi := &file_reconnaissance_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *TargetInfo) String() string { return protoimpl.X.MessageStringOf(x) } func (*TargetInfo) ProtoMessage() {} func (x *TargetInfo) ProtoReflect() protoreflect.Message { mi := &file_reconnaissance_proto_msgTypes[0] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } func (x *TargetInfo) GetNetworkEndpoints() []*network_go_proto.NetworkEndpoint { if x != nil { if x.xxx_hidden_NetworkEndpoints != nil { return *x.xxx_hidden_NetworkEndpoints } } return nil } func (x *TargetInfo) GetOperatingSystemClasses() []*OperatingSystemClass { if x != nil { if x.xxx_hidden_OperatingSystemClasses != nil { return *x.xxx_hidden_OperatingSystemClasses } } return nil } func (x *TargetInfo) SetNetworkEndpoints(v []*network_go_proto.NetworkEndpoint) { x.xxx_hidden_NetworkEndpoints = &v } func (x *TargetInfo) SetOperatingSystemClasses(v []*OperatingSystemClass) { x.xxx_hidden_OperatingSystemClasses = &v } type TargetInfo_builder struct { _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. // All the known network endpoints of the scanning target. NetworkEndpoints []*network_go_proto.NetworkEndpoint OperatingSystemClasses []*OperatingSystemClass } func (b0 TargetInfo_builder) Build() *TargetInfo { m0 := &TargetInfo{} b, x := &b0, m0 _, _ = b, x x.xxx_hidden_NetworkEndpoints = &b.NetworkEndpoints x.xxx_hidden_OperatingSystemClasses = &b.OperatingSystemClasses return m0 } // Represents a ForOperatingSystem annotation placeholder used by the // PluginDefinition proto above. // For possible values, consult the following database: // https://raw.githubusercontent.com/nmap/nmap/master/nmap-os-db type OperatingSystemClass struct { state protoimpl.MessageState `protogen:"opaque.v1"` xxx_hidden_Type string `protobuf:"bytes,1,opt,name=type,proto3"` xxx_hidden_Vendor string `protobuf:"bytes,2,opt,name=vendor,proto3"` xxx_hidden_OsFamily string `protobuf:"bytes,3,opt,name=os_family,json=osFamily,proto3"` xxx_hidden_OsGeneration string `protobuf:"bytes,4,opt,name=os_generation,json=osGeneration,proto3"` xxx_hidden_Accuracy uint32 `protobuf:"varint,5,opt,name=accuracy,proto3"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *OperatingSystemClass) Reset() { *x = OperatingSystemClass{} mi := &file_reconnaissance_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *OperatingSystemClass) String() string { return protoimpl.X.MessageStringOf(x) } func (*OperatingSystemClass) ProtoMessage() {} func (x *OperatingSystemClass) ProtoReflect() protoreflect.Message { mi := &file_reconnaissance_proto_msgTypes[1] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } func (x *OperatingSystemClass) GetType() string { if x != nil { return x.xxx_hidden_Type } return "" } func (x *OperatingSystemClass) GetVendor() string { if x != nil { return x.xxx_hidden_Vendor } return "" } func (x *OperatingSystemClass) GetOsFamily() string { if x != nil { return x.xxx_hidden_OsFamily } return "" } func (x *OperatingSystemClass) GetOsGeneration() string { if x != nil { return x.xxx_hidden_OsGeneration } return "" } func (x *OperatingSystemClass) GetAccuracy() uint32 { if x != nil { return x.xxx_hidden_Accuracy } return 0 } func (x *OperatingSystemClass) SetType(v string) { x.xxx_hidden_Type = v } func (x *OperatingSystemClass) SetVendor(v string) { x.xxx_hidden_Vendor = v } func (x *OperatingSystemClass) SetOsFamily(v string) { x.xxx_hidden_OsFamily = v } func (x *OperatingSystemClass) SetOsGeneration(v string) { x.xxx_hidden_OsGeneration = v } func (x *OperatingSystemClass) SetAccuracy(v uint32) { x.xxx_hidden_Accuracy = v } type OperatingSystemClass_builder struct { _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. // The type of the target operating system, e.g. "general purpose" Type string // The vendor of the target operating system, e.g. "Linux" Vendor string // The family of the target operating system, e.g. "Linux" OsFamily string // The generation of the target operating system, e.g. "2.6.X" OsGeneration string // The estimated accuracy of the target operating system, e.g. 90 Accuracy uint32 } func (b0 OperatingSystemClass_builder) Build() *OperatingSystemClass { m0 := &OperatingSystemClass{} b, x := &b0, m0 _, _ = b, x x.xxx_hidden_Type = b.Type x.xxx_hidden_Vendor = b.Vendor x.xxx_hidden_OsFamily = b.OsFamily x.xxx_hidden_OsGeneration = b.OsGeneration x.xxx_hidden_Accuracy = b.Accuracy return m0 } // Report from a port scanner. type PortScanningReport struct { state protoimpl.MessageState `protogen:"opaque.v1"` xxx_hidden_TargetInfo *TargetInfo `protobuf:"bytes,1,opt,name=target_info,json=targetInfo,proto3"` xxx_hidden_NetworkServices *[]*network_service_go_proto.NetworkService `protobuf:"bytes,2,rep,name=network_services,json=networkServices,proto3"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *PortScanningReport) Reset() { *x = PortScanningReport{} mi := &file_reconnaissance_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *PortScanningReport) String() string { return protoimpl.X.MessageStringOf(x) } func (*PortScanningReport) ProtoMessage() {} func (x *PortScanningReport) ProtoReflect() protoreflect.Message { mi := &file_reconnaissance_proto_msgTypes[2] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } func (x *PortScanningReport) GetTargetInfo() *TargetInfo { if x != nil { return x.xxx_hidden_TargetInfo } return nil } func (x *PortScanningReport) GetNetworkServices() []*network_service_go_proto.NetworkService { if x != nil { if x.xxx_hidden_NetworkServices != nil { return *x.xxx_hidden_NetworkServices } } return nil } func (x *PortScanningReport) SetTargetInfo(v *TargetInfo) { x.xxx_hidden_TargetInfo = v } func (x *PortScanningReport) SetNetworkServices(v []*network_service_go_proto.NetworkService) { x.xxx_hidden_NetworkServices = &v } func (x *PortScanningReport) HasTargetInfo() bool { if x == nil { return false } return x.xxx_hidden_TargetInfo != nil } func (x *PortScanningReport) ClearTargetInfo() { x.xxx_hidden_TargetInfo = nil } type PortScanningReport_builder struct { _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. // Information about the scanning target. TargetInfo *TargetInfo // List of all the exposed network services. NetworkServices []*network_service_go_proto.NetworkService } func (b0 PortScanningReport_builder) Build() *PortScanningReport { m0 := &PortScanningReport{} b, x := &b0, m0 _, _ = b, x x.xxx_hidden_TargetInfo = b.TargetInfo x.xxx_hidden_NetworkServices = &b.NetworkServices return m0 } // Report from a service fingerprinter. type FingerprintingReport struct { state protoimpl.MessageState `protogen:"opaque.v1"` xxx_hidden_NetworkServices *[]*network_service_go_proto.NetworkService `protobuf:"bytes,3,rep,name=network_services,json=networkServices,proto3"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *FingerprintingReport) Reset() { *x = FingerprintingReport{} mi := &file_reconnaissance_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *FingerprintingReport) String() string { return protoimpl.X.MessageStringOf(x) } func (*FingerprintingReport) ProtoMessage() {} func (x *FingerprintingReport) ProtoReflect() protoreflect.Message { mi := &file_reconnaissance_proto_msgTypes[3] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } func (x *FingerprintingReport) GetNetworkServices() []*network_service_go_proto.NetworkService { if x != nil { if x.xxx_hidden_NetworkServices != nil { return *x.xxx_hidden_NetworkServices } } return nil } func (x *FingerprintingReport) SetNetworkServices(v []*network_service_go_proto.NetworkService) { x.xxx_hidden_NetworkServices = &v } type FingerprintingReport_builder struct { _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. // List of all the identified network services after fingerprinting. NetworkServices []*network_service_go_proto.NetworkService } func (b0 FingerprintingReport_builder) Build() *FingerprintingReport { m0 := &FingerprintingReport{} b, x := &b0, m0 _, _ = b, x x.xxx_hidden_NetworkServices = &b.NetworkServices return m0 } // Full reconnaissance report about a single scanning target. type ReconnaissanceReport struct { state protoimpl.MessageState `protogen:"opaque.v1"` xxx_hidden_TargetInfo *TargetInfo `protobuf:"bytes,1,opt,name=target_info,json=targetInfo,proto3"` xxx_hidden_NetworkServices *[]*network_service_go_proto.NetworkService `protobuf:"bytes,2,rep,name=network_services,json=networkServices,proto3"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *ReconnaissanceReport) Reset() { *x = ReconnaissanceReport{} mi := &file_reconnaissance_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *ReconnaissanceReport) String() string { return protoimpl.X.MessageStringOf(x) } func (*ReconnaissanceReport) ProtoMessage() {} func (x *ReconnaissanceReport) ProtoReflect() protoreflect.Message { mi := &file_reconnaissance_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } func (x *ReconnaissanceReport) GetTargetInfo() *TargetInfo { if x != nil { return x.xxx_hidden_TargetInfo } return nil } func (x *ReconnaissanceReport) GetNetworkServices() []*network_service_go_proto.NetworkService { if x != nil { if x.xxx_hidden_NetworkServices != nil { return *x.xxx_hidden_NetworkServices } } return nil } func (x *ReconnaissanceReport) SetTargetInfo(v *TargetInfo) { x.xxx_hidden_TargetInfo = v } func (x *ReconnaissanceReport) SetNetworkServices(v []*network_service_go_proto.NetworkService) { x.xxx_hidden_NetworkServices = &v } func (x *ReconnaissanceReport) HasTargetInfo() bool { if x == nil { return false } return x.xxx_hidden_TargetInfo != nil } func (x *ReconnaissanceReport) ClearTargetInfo() { x.xxx_hidden_TargetInfo = nil } type ReconnaissanceReport_builder struct { _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. // Information about the scanning target. TargetInfo *TargetInfo // All exposed network services of the scanning target. NetworkServices []*network_service_go_proto.NetworkService } func (b0 ReconnaissanceReport_builder) Build() *ReconnaissanceReport { m0 := &ReconnaissanceReport{} b, x := &b0, m0 _, _ = b, x x.xxx_hidden_TargetInfo = b.TargetInfo x.xxx_hidden_NetworkServices = &b.NetworkServices return m0 } var File_reconnaissance_proto protoreflect.FileDescriptor const file_reconnaissance_proto_rawDesc = "" + "\n" + "\x14reconnaissance.proto\x12\rtsunami.proto\x1a\rnetwork.proto\x1a\x15network_service.proto\"\xb8\x01\n" + "\n" + "TargetInfo\x12K\n" + "\x11network_endpoints\x18\x01 \x03(\v2\x1e.tsunami.proto.NetworkEndpointR\x10networkEndpoints\x12]\n" + "\x18operating_system_classes\x18\x02 \x03(\v2#.tsunami.proto.OperatingSystemClassR\x16operatingSystemClasses\"\xa0\x01\n" + "\x14OperatingSystemClass\x12\x12\n" + "\x04type\x18\x01 \x01(\tR\x04type\x12\x16\n" + "\x06vendor\x18\x02 \x01(\tR\x06vendor\x12\x1b\n" + "\tos_family\x18\x03 \x01(\tR\bosFamily\x12#\n" + "\ros_generation\x18\x04 \x01(\tR\fosGeneration\x12\x1a\n" + "\baccuracy\x18\x05 \x01(\rR\baccuracy\"\x9a\x01\n" + "\x12PortScanningReport\x12:\n" + "\vtarget_info\x18\x01 \x01(\v2\x19.tsunami.proto.TargetInfoR\n" + "targetInfo\x12H\n" + "\x10network_services\x18\x02 \x03(\v2\x1d.tsunami.proto.NetworkServiceR\x0fnetworkServices\"`\n" + "\x14FingerprintingReport\x12H\n" + "\x10network_services\x18\x03 \x03(\v2\x1d.tsunami.proto.NetworkServiceR\x0fnetworkServices\"\x9c\x01\n" + "\x14ReconnaissanceReport\x12:\n" + "\vtarget_info\x18\x01 \x01(\v2\x19.tsunami.proto.TargetInfoR\n" + "targetInfo\x12H\n" + "\x10network_services\x18\x02 \x03(\v2\x1d.tsunami.proto.NetworkServiceR\x0fnetworkServicesB\x7f\n" + "\x18com.google.tsunami.protoB\x14ReconnaissanceProtosP\x01ZKgithub.com/google/tsunami-security-scanner/proto/go/reconnaissance_go_protob\x06proto3" var file_reconnaissance_proto_msgTypes = make([]protoimpl.MessageInfo, 5) var file_reconnaissance_proto_goTypes = []any{ (*TargetInfo)(nil), // 0: tsunami.proto.TargetInfo (*OperatingSystemClass)(nil), // 1: tsunami.proto.OperatingSystemClass (*PortScanningReport)(nil), // 2: tsunami.proto.PortScanningReport (*FingerprintingReport)(nil), // 3: tsunami.proto.FingerprintingReport (*ReconnaissanceReport)(nil), // 4: tsunami.proto.ReconnaissanceReport (*network_go_proto.NetworkEndpoint)(nil), // 5: tsunami.proto.NetworkEndpoint (*network_service_go_proto.NetworkService)(nil), // 6: tsunami.proto.NetworkService } var file_reconnaissance_proto_depIdxs = []int32{ 5, // 0: tsunami.proto.TargetInfo.network_endpoints:type_name -> tsunami.proto.NetworkEndpoint 1, // 1: tsunami.proto.TargetInfo.operating_system_classes:type_name -> tsunami.proto.OperatingSystemClass 0, // 2: tsunami.proto.PortScanningReport.target_info:type_name -> tsunami.proto.TargetInfo 6, // 3: tsunami.proto.PortScanningReport.network_services:type_name -> tsunami.proto.NetworkService 6, // 4: tsunami.proto.FingerprintingReport.network_services:type_name -> tsunami.proto.NetworkService 0, // 5: tsunami.proto.ReconnaissanceReport.target_info:type_name -> tsunami.proto.TargetInfo 6, // 6: tsunami.proto.ReconnaissanceReport.network_services:type_name -> tsunami.proto.NetworkService 7, // [7:7] is the sub-list for method output_type 7, // [7:7] is the sub-list for method input_type 7, // [7:7] is the sub-list for extension type_name 7, // [7:7] is the sub-list for extension extendee 0, // [0:7] is the sub-list for field type_name } func init() { file_reconnaissance_proto_init() } func file_reconnaissance_proto_init() { if File_reconnaissance_proto != nil { return } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_reconnaissance_proto_rawDesc), len(file_reconnaissance_proto_rawDesc)), NumEnums: 0, NumMessages: 5, NumExtensions: 0, NumServices: 0, }, GoTypes: file_reconnaissance_proto_goTypes, DependencyIndexes: file_reconnaissance_proto_depIdxs, MessageInfos: file_reconnaissance_proto_msgTypes, }.Build() File_reconnaissance_proto = out.File file_reconnaissance_proto_goTypes = nil file_reconnaissance_proto_depIdxs = nil } ================================================ FILE: proto/go/scan_results_go_proto/scan_results.pb.go ================================================ // // Copyright 2020 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Data models for describing scanning results. // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.11 // protoc v3.21.12 // source: scan_results.proto package scan_results_go_proto import ( detection_go_proto "github.com/google/tsunami-security-scanner/proto/go/detection_go_proto" network_service_go_proto "github.com/google/tsunami-security-scanner/proto/go/network_service_go_proto" reconnaissance_go_proto "github.com/google/tsunami-security-scanner/proto/go/reconnaissance_go_proto" vulnerability_go_proto "github.com/google/tsunami-security-scanner/proto/go/vulnerability_go_proto" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" durationpb "google.golang.org/protobuf/types/known/durationpb" timestamppb "google.golang.org/protobuf/types/known/timestamppb" reflect "reflect" unsafe "unsafe" ) const ( // Verify that this generated code is sufficiently up-to-date. _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) // Verify that runtime/protoimpl is sufficiently up-to-date. _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) // Execution status of the scan. // NEXT ID: 5 type ScanStatus int32 const ( // Unspecified status. ScanStatus_SCAN_STATUS_UNSPECIFIED ScanStatus = 0 // Scan finished successfully. ScanStatus_SUCCEEDED ScanStatus = 1 // Scan finished with only a small set of selected detectors succeeded. ScanStatus_PARTIALLY_SUCCEEDED ScanStatus = 4 // Scan failed. ScanStatus_FAILED ScanStatus = 2 // Scan cancelled. ScanStatus_CANCELLED ScanStatus = 3 ) // Enum value maps for ScanStatus. var ( ScanStatus_name = map[int32]string{ 0: "SCAN_STATUS_UNSPECIFIED", 1: "SUCCEEDED", 4: "PARTIALLY_SUCCEEDED", 2: "FAILED", 3: "CANCELLED", } ScanStatus_value = map[string]int32{ "SCAN_STATUS_UNSPECIFIED": 0, "SUCCEEDED": 1, "PARTIALLY_SUCCEEDED": 4, "FAILED": 2, "CANCELLED": 3, } ) func (x ScanStatus) Enum() *ScanStatus { p := new(ScanStatus) *p = x return p } func (x ScanStatus) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (ScanStatus) Descriptor() protoreflect.EnumDescriptor { return file_scan_results_proto_enumTypes[0].Descriptor() } func (ScanStatus) Type() protoreflect.EnumType { return &file_scan_results_proto_enumTypes[0] } func (x ScanStatus) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } // A single vulnerability finding for a specific service. type ScanFinding struct { state protoimpl.MessageState `protogen:"opaque.v1"` xxx_hidden_TargetInfo *reconnaissance_go_proto.TargetInfo `protobuf:"bytes,1,opt,name=target_info,json=targetInfo,proto3"` xxx_hidden_NetworkService *network_service_go_proto.NetworkService `protobuf:"bytes,2,opt,name=network_service,json=networkService,proto3"` xxx_hidden_Vulnerability *vulnerability_go_proto.Vulnerability `protobuf:"bytes,3,opt,name=vulnerability,proto3"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *ScanFinding) Reset() { *x = ScanFinding{} mi := &file_scan_results_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *ScanFinding) String() string { return protoimpl.X.MessageStringOf(x) } func (*ScanFinding) ProtoMessage() {} func (x *ScanFinding) ProtoReflect() protoreflect.Message { mi := &file_scan_results_proto_msgTypes[0] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } func (x *ScanFinding) GetTargetInfo() *reconnaissance_go_proto.TargetInfo { if x != nil { return x.xxx_hidden_TargetInfo } return nil } func (x *ScanFinding) GetNetworkService() *network_service_go_proto.NetworkService { if x != nil { return x.xxx_hidden_NetworkService } return nil } func (x *ScanFinding) GetVulnerability() *vulnerability_go_proto.Vulnerability { if x != nil { return x.xxx_hidden_Vulnerability } return nil } func (x *ScanFinding) SetTargetInfo(v *reconnaissance_go_proto.TargetInfo) { x.xxx_hidden_TargetInfo = v } func (x *ScanFinding) SetNetworkService(v *network_service_go_proto.NetworkService) { x.xxx_hidden_NetworkService = v } func (x *ScanFinding) SetVulnerability(v *vulnerability_go_proto.Vulnerability) { x.xxx_hidden_Vulnerability = v } func (x *ScanFinding) HasTargetInfo() bool { if x == nil { return false } return x.xxx_hidden_TargetInfo != nil } func (x *ScanFinding) HasNetworkService() bool { if x == nil { return false } return x.xxx_hidden_NetworkService != nil } func (x *ScanFinding) HasVulnerability() bool { if x == nil { return false } return x.xxx_hidden_Vulnerability != nil } func (x *ScanFinding) ClearTargetInfo() { x.xxx_hidden_TargetInfo = nil } func (x *ScanFinding) ClearNetworkService() { x.xxx_hidden_NetworkService = nil } func (x *ScanFinding) ClearVulnerability() { x.xxx_hidden_Vulnerability = nil } type ScanFinding_builder struct { _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. // Information about the scanned target. TargetInfo *reconnaissance_go_proto.TargetInfo // Information about the scanned network service. NetworkService *network_service_go_proto.NetworkService // Details about the detected vulnerability. Vulnerability *vulnerability_go_proto.Vulnerability } func (b0 ScanFinding_builder) Build() *ScanFinding { m0 := &ScanFinding{} b, x := &b0, m0 _, _ = b, x x.xxx_hidden_TargetInfo = b.TargetInfo x.xxx_hidden_NetworkService = b.NetworkService x.xxx_hidden_Vulnerability = b.Vulnerability return m0 } // Full scanning results. // NEXT ID: 9 type ScanResults struct { state protoimpl.MessageState `protogen:"opaque.v1"` xxx_hidden_ScanStatus ScanStatus `protobuf:"varint,1,opt,name=scan_status,json=scanStatus,proto3,enum=tsunami.proto.ScanStatus"` xxx_hidden_StatusMessage string `protobuf:"bytes,6,opt,name=status_message,json=statusMessage,proto3"` xxx_hidden_TargetAlive bool `protobuf:"varint,8,opt,name=target_alive,json=targetAlive,proto3"` xxx_hidden_ScanFindings *[]*ScanFinding `protobuf:"bytes,2,rep,name=scan_findings,json=scanFindings,proto3"` xxx_hidden_ScanStartTimestamp *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=scan_start_timestamp,json=scanStartTimestamp,proto3"` xxx_hidden_ScanDuration *durationpb.Duration `protobuf:"bytes,4,opt,name=scan_duration,json=scanDuration,proto3"` xxx_hidden_FullDetectionReports *FullDetectionReports `protobuf:"bytes,5,opt,name=full_detection_reports,json=fullDetectionReports,proto3"` xxx_hidden_ReconnaissanceReport *reconnaissance_go_proto.ReconnaissanceReport `protobuf:"bytes,7,opt,name=reconnaissance_report,json=reconnaissanceReport,proto3"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *ScanResults) Reset() { *x = ScanResults{} mi := &file_scan_results_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *ScanResults) String() string { return protoimpl.X.MessageStringOf(x) } func (*ScanResults) ProtoMessage() {} func (x *ScanResults) ProtoReflect() protoreflect.Message { mi := &file_scan_results_proto_msgTypes[1] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } func (x *ScanResults) GetScanStatus() ScanStatus { if x != nil { return x.xxx_hidden_ScanStatus } return ScanStatus_SCAN_STATUS_UNSPECIFIED } func (x *ScanResults) GetStatusMessage() string { if x != nil { return x.xxx_hidden_StatusMessage } return "" } func (x *ScanResults) GetTargetAlive() bool { if x != nil { return x.xxx_hidden_TargetAlive } return false } func (x *ScanResults) GetScanFindings() []*ScanFinding { if x != nil { if x.xxx_hidden_ScanFindings != nil { return *x.xxx_hidden_ScanFindings } } return nil } func (x *ScanResults) GetScanStartTimestamp() *timestamppb.Timestamp { if x != nil { return x.xxx_hidden_ScanStartTimestamp } return nil } func (x *ScanResults) GetScanDuration() *durationpb.Duration { if x != nil { return x.xxx_hidden_ScanDuration } return nil } func (x *ScanResults) GetFullDetectionReports() *FullDetectionReports { if x != nil { return x.xxx_hidden_FullDetectionReports } return nil } func (x *ScanResults) GetReconnaissanceReport() *reconnaissance_go_proto.ReconnaissanceReport { if x != nil { return x.xxx_hidden_ReconnaissanceReport } return nil } func (x *ScanResults) SetScanStatus(v ScanStatus) { x.xxx_hidden_ScanStatus = v } func (x *ScanResults) SetStatusMessage(v string) { x.xxx_hidden_StatusMessage = v } func (x *ScanResults) SetTargetAlive(v bool) { x.xxx_hidden_TargetAlive = v } func (x *ScanResults) SetScanFindings(v []*ScanFinding) { x.xxx_hidden_ScanFindings = &v } func (x *ScanResults) SetScanStartTimestamp(v *timestamppb.Timestamp) { x.xxx_hidden_ScanStartTimestamp = v } func (x *ScanResults) SetScanDuration(v *durationpb.Duration) { x.xxx_hidden_ScanDuration = v } func (x *ScanResults) SetFullDetectionReports(v *FullDetectionReports) { x.xxx_hidden_FullDetectionReports = v } func (x *ScanResults) SetReconnaissanceReport(v *reconnaissance_go_proto.ReconnaissanceReport) { x.xxx_hidden_ReconnaissanceReport = v } func (x *ScanResults) HasScanStartTimestamp() bool { if x == nil { return false } return x.xxx_hidden_ScanStartTimestamp != nil } func (x *ScanResults) HasScanDuration() bool { if x == nil { return false } return x.xxx_hidden_ScanDuration != nil } func (x *ScanResults) HasFullDetectionReports() bool { if x == nil { return false } return x.xxx_hidden_FullDetectionReports != nil } func (x *ScanResults) HasReconnaissanceReport() bool { if x == nil { return false } return x.xxx_hidden_ReconnaissanceReport != nil } func (x *ScanResults) ClearScanStartTimestamp() { x.xxx_hidden_ScanStartTimestamp = nil } func (x *ScanResults) ClearScanDuration() { x.xxx_hidden_ScanDuration = nil } func (x *ScanResults) ClearFullDetectionReports() { x.xxx_hidden_FullDetectionReports = nil } func (x *ScanResults) ClearReconnaissanceReport() { x.xxx_hidden_ReconnaissanceReport = nil } type ScanResults_builder struct { _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. // Status of this scan. ScanStatus ScanStatus // Detailed message for the scan status. StatusMessage string // Reports whether the target was alive during the scan. // A target is considered alive if at least one network service was identified // or at least one vulnerability was detected. TargetAlive bool // All findings from this scan. ScanFindings []*ScanFinding // Time when this scan was started. ScanStartTimestamp *timestamppb.Timestamp // Duration of the full scan. ScanDuration *durationpb.Duration // Detection reports from all triggered Tsunami detection plugins. FullDetectionReports *FullDetectionReports // Reconnaissance reports from the fingerprinting stage. ReconnaissanceReport *reconnaissance_go_proto.ReconnaissanceReport } func (b0 ScanResults_builder) Build() *ScanResults { m0 := &ScanResults{} b, x := &b0, m0 _, _ = b, x x.xxx_hidden_ScanStatus = b.ScanStatus x.xxx_hidden_StatusMessage = b.StatusMessage x.xxx_hidden_TargetAlive = b.TargetAlive x.xxx_hidden_ScanFindings = &b.ScanFindings x.xxx_hidden_ScanStartTimestamp = b.ScanStartTimestamp x.xxx_hidden_ScanDuration = b.ScanDuration x.xxx_hidden_FullDetectionReports = b.FullDetectionReports x.xxx_hidden_ReconnaissanceReport = b.ReconnaissanceReport return m0 } // Full detection reports from all triggered Tsunami detection plugins. type FullDetectionReports struct { state protoimpl.MessageState `protogen:"opaque.v1"` xxx_hidden_DetectionReports *[]*detection_go_proto.DetectionReport `protobuf:"bytes,1,rep,name=detection_reports,json=detectionReports,proto3"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *FullDetectionReports) Reset() { *x = FullDetectionReports{} mi := &file_scan_results_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *FullDetectionReports) String() string { return protoimpl.X.MessageStringOf(x) } func (*FullDetectionReports) ProtoMessage() {} func (x *FullDetectionReports) ProtoReflect() protoreflect.Message { mi := &file_scan_results_proto_msgTypes[2] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } func (x *FullDetectionReports) GetDetectionReports() []*detection_go_proto.DetectionReport { if x != nil { if x.xxx_hidden_DetectionReports != nil { return *x.xxx_hidden_DetectionReports } } return nil } func (x *FullDetectionReports) SetDetectionReports(v []*detection_go_proto.DetectionReport) { x.xxx_hidden_DetectionReports = &v } type FullDetectionReports_builder struct { _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. DetectionReports []*detection_go_proto.DetectionReport } func (b0 FullDetectionReports_builder) Build() *FullDetectionReports { m0 := &FullDetectionReports{} b, x := &b0, m0 _, _ = b, x x.xxx_hidden_DetectionReports = &b.DetectionReports return m0 } var File_scan_results_proto protoreflect.FileDescriptor const file_scan_results_proto_rawDesc = "" + "\n" + "\x12scan_results.proto\x12\rtsunami.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1egoogle/protobuf/duration.proto\x1a\x0fdetection.proto\x1a\x15network_service.proto\x1a\x14reconnaissance.proto\x1a\x13vulnerability.proto\"\xd5\x01\n" + "\vScanFinding\x12:\n" + "\vtarget_info\x18\x01 \x01(\v2\x19.tsunami.proto.TargetInfoR\n" + "targetInfo\x12F\n" + "\x0fnetwork_service\x18\x02 \x01(\v2\x1d.tsunami.proto.NetworkServiceR\x0enetworkService\x12B\n" + "\rvulnerability\x18\x03 \x01(\v2\x1c.tsunami.proto.VulnerabilityR\rvulnerability\"\x97\x04\n" + "\vScanResults\x12:\n" + "\vscan_status\x18\x01 \x01(\x0e2\x19.tsunami.proto.ScanStatusR\n" + "scanStatus\x12%\n" + "\x0estatus_message\x18\x06 \x01(\tR\rstatusMessage\x12!\n" + "\ftarget_alive\x18\b \x01(\bR\vtargetAlive\x12?\n" + "\rscan_findings\x18\x02 \x03(\v2\x1a.tsunami.proto.ScanFindingR\fscanFindings\x12L\n" + "\x14scan_start_timestamp\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\x12scanStartTimestamp\x12>\n" + "\rscan_duration\x18\x04 \x01(\v2\x19.google.protobuf.DurationR\fscanDuration\x12Y\n" + "\x16full_detection_reports\x18\x05 \x01(\v2#.tsunami.proto.FullDetectionReportsR\x14fullDetectionReports\x12X\n" + "\x15reconnaissance_report\x18\a \x01(\v2#.tsunami.proto.ReconnaissanceReportR\x14reconnaissanceReport\"c\n" + "\x14FullDetectionReports\x12K\n" + "\x11detection_reports\x18\x01 \x03(\v2\x1e.tsunami.proto.DetectionReportR\x10detectionReports*l\n" + "\n" + "ScanStatus\x12\x1b\n" + "\x17SCAN_STATUS_UNSPECIFIED\x10\x00\x12\r\n" + "\tSUCCEEDED\x10\x01\x12\x17\n" + "\x13PARTIALLY_SUCCEEDED\x10\x04\x12\n" + "\n" + "\x06FAILED\x10\x02\x12\r\n" + "\tCANCELLED\x10\x03Bz\n" + "\x18com.google.tsunami.protoB\x11ScanResultsProtosP\x01ZIgithub.com/google/tsunami-security-scanner/proto/go/scan_results_go_protob\x06proto3" var file_scan_results_proto_enumTypes = make([]protoimpl.EnumInfo, 1) var file_scan_results_proto_msgTypes = make([]protoimpl.MessageInfo, 3) var file_scan_results_proto_goTypes = []any{ (ScanStatus)(0), // 0: tsunami.proto.ScanStatus (*ScanFinding)(nil), // 1: tsunami.proto.ScanFinding (*ScanResults)(nil), // 2: tsunami.proto.ScanResults (*FullDetectionReports)(nil), // 3: tsunami.proto.FullDetectionReports (*reconnaissance_go_proto.TargetInfo)(nil), // 4: tsunami.proto.TargetInfo (*network_service_go_proto.NetworkService)(nil), // 5: tsunami.proto.NetworkService (*vulnerability_go_proto.Vulnerability)(nil), // 6: tsunami.proto.Vulnerability (*timestamppb.Timestamp)(nil), // 7: google.protobuf.Timestamp (*durationpb.Duration)(nil), // 8: google.protobuf.Duration (*reconnaissance_go_proto.ReconnaissanceReport)(nil), // 9: tsunami.proto.ReconnaissanceReport (*detection_go_proto.DetectionReport)(nil), // 10: tsunami.proto.DetectionReport } var file_scan_results_proto_depIdxs = []int32{ 4, // 0: tsunami.proto.ScanFinding.target_info:type_name -> tsunami.proto.TargetInfo 5, // 1: tsunami.proto.ScanFinding.network_service:type_name -> tsunami.proto.NetworkService 6, // 2: tsunami.proto.ScanFinding.vulnerability:type_name -> tsunami.proto.Vulnerability 0, // 3: tsunami.proto.ScanResults.scan_status:type_name -> tsunami.proto.ScanStatus 1, // 4: tsunami.proto.ScanResults.scan_findings:type_name -> tsunami.proto.ScanFinding 7, // 5: tsunami.proto.ScanResults.scan_start_timestamp:type_name -> google.protobuf.Timestamp 8, // 6: tsunami.proto.ScanResults.scan_duration:type_name -> google.protobuf.Duration 3, // 7: tsunami.proto.ScanResults.full_detection_reports:type_name -> tsunami.proto.FullDetectionReports 9, // 8: tsunami.proto.ScanResults.reconnaissance_report:type_name -> tsunami.proto.ReconnaissanceReport 10, // 9: tsunami.proto.FullDetectionReports.detection_reports:type_name -> tsunami.proto.DetectionReport 10, // [10:10] is the sub-list for method output_type 10, // [10:10] is the sub-list for method input_type 10, // [10:10] is the sub-list for extension type_name 10, // [10:10] is the sub-list for extension extendee 0, // [0:10] is the sub-list for field type_name } func init() { file_scan_results_proto_init() } func file_scan_results_proto_init() { if File_scan_results_proto != nil { return } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_scan_results_proto_rawDesc), len(file_scan_results_proto_rawDesc)), NumEnums: 1, NumMessages: 3, NumExtensions: 0, NumServices: 0, }, GoTypes: file_scan_results_proto_goTypes, DependencyIndexes: file_scan_results_proto_depIdxs, EnumInfos: file_scan_results_proto_enumTypes, MessageInfos: file_scan_results_proto_msgTypes, }.Build() File_scan_results_proto = out.File file_scan_results_proto_goTypes = nil file_scan_results_proto_depIdxs = nil } ================================================ FILE: proto/go/scan_target_go_proto/scan_target.pb.go ================================================ // // Copyright 2020 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Data models for describing a scanning target. // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.11 // protoc v3.21.12 // source: scan_target.proto package scan_target_go_proto import ( network_go_proto "github.com/google/tsunami-security-scanner/proto/go/network_go_proto" network_service_go_proto "github.com/google/tsunami-security-scanner/proto/go/network_service_go_proto" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" unsafe "unsafe" ) const ( // Verify that this generated code is sufficiently up-to-date. _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) // Verify that runtime/protoimpl is sufficiently up-to-date. _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) // The information about a scan target. type ScanTarget struct { state protoimpl.MessageState `protogen:"opaque.v1"` xxx_hidden_Target isScanTarget_Target `protobuf_oneof:"target"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *ScanTarget) Reset() { *x = ScanTarget{} mi := &file_scan_target_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *ScanTarget) String() string { return protoimpl.X.MessageStringOf(x) } func (*ScanTarget) ProtoMessage() {} func (x *ScanTarget) ProtoReflect() protoreflect.Message { mi := &file_scan_target_proto_msgTypes[0] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } func (x *ScanTarget) GetNetworkEndpoint() *network_go_proto.NetworkEndpoint { if x != nil { if x, ok := x.xxx_hidden_Target.(*scanTarget_NetworkEndpoint); ok { return x.NetworkEndpoint } } return nil } func (x *ScanTarget) GetNetworkService() *network_service_go_proto.NetworkService { if x != nil { if x, ok := x.xxx_hidden_Target.(*scanTarget_NetworkService); ok { return x.NetworkService } } return nil } func (x *ScanTarget) SetNetworkEndpoint(v *network_go_proto.NetworkEndpoint) { if v == nil { x.xxx_hidden_Target = nil return } x.xxx_hidden_Target = &scanTarget_NetworkEndpoint{v} } func (x *ScanTarget) SetNetworkService(v *network_service_go_proto.NetworkService) { if v == nil { x.xxx_hidden_Target = nil return } x.xxx_hidden_Target = &scanTarget_NetworkService{v} } func (x *ScanTarget) HasTarget() bool { if x == nil { return false } return x.xxx_hidden_Target != nil } func (x *ScanTarget) HasNetworkEndpoint() bool { if x == nil { return false } _, ok := x.xxx_hidden_Target.(*scanTarget_NetworkEndpoint) return ok } func (x *ScanTarget) HasNetworkService() bool { if x == nil { return false } _, ok := x.xxx_hidden_Target.(*scanTarget_NetworkService) return ok } func (x *ScanTarget) ClearTarget() { x.xxx_hidden_Target = nil } func (x *ScanTarget) ClearNetworkEndpoint() { if _, ok := x.xxx_hidden_Target.(*scanTarget_NetworkEndpoint); ok { x.xxx_hidden_Target = nil } } func (x *ScanTarget) ClearNetworkService() { if _, ok := x.xxx_hidden_Target.(*scanTarget_NetworkService); ok { x.xxx_hidden_Target = nil } } const ScanTarget_Target_not_set_case case_ScanTarget_Target = 0 const ScanTarget_NetworkEndpoint_case case_ScanTarget_Target = 1 const ScanTarget_NetworkService_case case_ScanTarget_Target = 2 func (x *ScanTarget) WhichTarget() case_ScanTarget_Target { if x == nil { return ScanTarget_Target_not_set_case } switch x.xxx_hidden_Target.(type) { case *scanTarget_NetworkEndpoint: return ScanTarget_NetworkEndpoint_case case *scanTarget_NetworkService: return ScanTarget_NetworkService_case default: return ScanTarget_Target_not_set_case } } type ScanTarget_builder struct { _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. // Fields of oneof xxx_hidden_Target: // The network endpoint to be scanned. NetworkEndpoint *network_go_proto.NetworkEndpoint // The network service to be scanned. NetworkService *network_service_go_proto.NetworkService // -- end of xxx_hidden_Target } func (b0 ScanTarget_builder) Build() *ScanTarget { m0 := &ScanTarget{} b, x := &b0, m0 _, _ = b, x if b.NetworkEndpoint != nil { x.xxx_hidden_Target = &scanTarget_NetworkEndpoint{b.NetworkEndpoint} } if b.NetworkService != nil { x.xxx_hidden_Target = &scanTarget_NetworkService{b.NetworkService} } return m0 } type case_ScanTarget_Target protoreflect.FieldNumber func (x case_ScanTarget_Target) String() string { md := file_scan_target_proto_msgTypes[0].Descriptor() if x == 0 { return "not set" } return protoimpl.X.MessageFieldStringOf(md, protoreflect.FieldNumber(x)) } type isScanTarget_Target interface { isScanTarget_Target() } type scanTarget_NetworkEndpoint struct { // The network endpoint to be scanned. NetworkEndpoint *network_go_proto.NetworkEndpoint `protobuf:"bytes,1,opt,name=network_endpoint,json=networkEndpoint,proto3,oneof"` } type scanTarget_NetworkService struct { // The network service to be scanned. NetworkService *network_service_go_proto.NetworkService `protobuf:"bytes,2,opt,name=network_service,json=networkService,proto3,oneof"` } func (*scanTarget_NetworkEndpoint) isScanTarget_Target() {} func (*scanTarget_NetworkService) isScanTarget_Target() {} var File_scan_target_proto protoreflect.FileDescriptor const file_scan_target_proto_rawDesc = "" + "\n" + "\x11scan_target.proto\x12\rtsunami.proto\x1a\rnetwork.proto\x1a\x15network_service.proto\"\xad\x01\n" + "\n" + "ScanTarget\x12K\n" + "\x10network_endpoint\x18\x01 \x01(\v2\x1e.tsunami.proto.NetworkEndpointH\x00R\x0fnetworkEndpoint\x12H\n" + "\x0fnetwork_service\x18\x02 \x01(\v2\x1d.tsunami.proto.NetworkServiceH\x00R\x0enetworkServiceB\b\n" + "\x06targetBx\n" + "\x18com.google.tsunami.protoB\x10ScanTargetProtosP\x01ZHgithub.com/google/tsunami-security-scanner/proto/go/scan_target_go_protob\x06proto3" var file_scan_target_proto_msgTypes = make([]protoimpl.MessageInfo, 1) var file_scan_target_proto_goTypes = []any{ (*ScanTarget)(nil), // 0: tsunami.proto.ScanTarget (*network_go_proto.NetworkEndpoint)(nil), // 1: tsunami.proto.NetworkEndpoint (*network_service_go_proto.NetworkService)(nil), // 2: tsunami.proto.NetworkService } var file_scan_target_proto_depIdxs = []int32{ 1, // 0: tsunami.proto.ScanTarget.network_endpoint:type_name -> tsunami.proto.NetworkEndpoint 2, // 1: tsunami.proto.ScanTarget.network_service:type_name -> tsunami.proto.NetworkService 2, // [2:2] is the sub-list for method output_type 2, // [2:2] is the sub-list for method input_type 2, // [2:2] is the sub-list for extension type_name 2, // [2:2] is the sub-list for extension extendee 0, // [0:2] is the sub-list for field type_name } func init() { file_scan_target_proto_init() } func file_scan_target_proto_init() { if File_scan_target_proto != nil { return } file_scan_target_proto_msgTypes[0].OneofWrappers = []any{ (*scanTarget_NetworkEndpoint)(nil), (*scanTarget_NetworkService)(nil), } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_scan_target_proto_rawDesc), len(file_scan_target_proto_rawDesc)), NumEnums: 0, NumMessages: 1, NumExtensions: 0, NumServices: 0, }, GoTypes: file_scan_target_proto_goTypes, DependencyIndexes: file_scan_target_proto_depIdxs, MessageInfos: file_scan_target_proto_msgTypes, }.Build() File_scan_target_proto = out.File file_scan_target_proto_goTypes = nil file_scan_target_proto_depIdxs = nil } ================================================ FILE: proto/go/software_go_proto/software.pb.go ================================================ // // Copyright 2019 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Data models for describing a software. // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.11 // protoc v3.21.12 // source: software.proto package software_go_proto import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" unsafe "unsafe" ) const ( // Verify that this generated code is sufficiently up-to-date. _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) // Verify that runtime/protoimpl is sufficiently up-to-date. _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) // Type of the Version message, identifying an ordinary software version or a // sentinel MINIMUM/MAXIMUM version. See comments below for what is a sentinel // version. type Version_VersionType int32 const ( Version_VERSION_TYPE_UNSPECIFIED Version_VersionType = 0 // A normal software version. Version_NORMAL Version_VersionType = 1 // A sentinel version representing negative infinity, i.e. MINIMUM version // is less than any NORMAL and MAXIMUM versions. Version_MINIMUM Version_VersionType = 2 // A sentinel version representing positive infinity, i.e. MAXIMUM version // is greater than any NORMAL and MINIMUM versions. Version_MAXIMUM Version_VersionType = 3 ) // Enum value maps for Version_VersionType. var ( Version_VersionType_name = map[int32]string{ 0: "VERSION_TYPE_UNSPECIFIED", 1: "NORMAL", 2: "MINIMUM", 3: "MAXIMUM", } Version_VersionType_value = map[string]int32{ "VERSION_TYPE_UNSPECIFIED": 0, "NORMAL": 1, "MINIMUM": 2, "MAXIMUM": 3, } ) func (x Version_VersionType) Enum() *Version_VersionType { p := new(Version_VersionType) *p = x return p } func (x Version_VersionType) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (Version_VersionType) Descriptor() protoreflect.EnumDescriptor { return file_software_proto_enumTypes[0].Descriptor() } func (Version_VersionType) Type() protoreflect.EnumType { return &file_software_proto_enumTypes[0] } func (x Version_VersionType) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } // Whether the range endpoint is inclusive or exclusive. type VersionRange_Inclusiveness int32 const ( VersionRange_INCLUSIVENESS_UNSPECIFIED VersionRange_Inclusiveness = 0 VersionRange_INCLUSIVE VersionRange_Inclusiveness = 1 VersionRange_EXCLUSIVE VersionRange_Inclusiveness = 2 ) // Enum value maps for VersionRange_Inclusiveness. var ( VersionRange_Inclusiveness_name = map[int32]string{ 0: "INCLUSIVENESS_UNSPECIFIED", 1: "INCLUSIVE", 2: "EXCLUSIVE", } VersionRange_Inclusiveness_value = map[string]int32{ "INCLUSIVENESS_UNSPECIFIED": 0, "INCLUSIVE": 1, "EXCLUSIVE": 2, } ) func (x VersionRange_Inclusiveness) Enum() *VersionRange_Inclusiveness { p := new(VersionRange_Inclusiveness) *p = x return p } func (x VersionRange_Inclusiveness) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (VersionRange_Inclusiveness) Descriptor() protoreflect.EnumDescriptor { return file_software_proto_enumTypes[1].Descriptor() } func (VersionRange_Inclusiveness) Type() protoreflect.EnumType { return &file_software_proto_enumTypes[1] } func (x VersionRange_Inclusiveness) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } // The exact version of a software. type Version struct { state protoimpl.MessageState `protogen:"opaque.v1"` xxx_hidden_Type Version_VersionType `protobuf:"varint,1,opt,name=type,proto3,enum=tsunami.proto.Version_VersionType"` xxx_hidden_FullVersionString string `protobuf:"bytes,2,opt,name=full_version_string,json=fullVersionString,proto3"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *Version) Reset() { *x = Version{} mi := &file_software_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *Version) String() string { return protoimpl.X.MessageStringOf(x) } func (*Version) ProtoMessage() {} func (x *Version) ProtoReflect() protoreflect.Message { mi := &file_software_proto_msgTypes[0] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } func (x *Version) GetType() Version_VersionType { if x != nil { return x.xxx_hidden_Type } return Version_VERSION_TYPE_UNSPECIFIED } func (x *Version) GetFullVersionString() string { if x != nil { return x.xxx_hidden_FullVersionString } return "" } func (x *Version) SetType(v Version_VersionType) { x.xxx_hidden_Type = v } func (x *Version) SetFullVersionString(v string) { x.xxx_hidden_FullVersionString = v } type Version_builder struct { _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. // Distinguishes between sentinel MIN/MAX versions and normal versions. Type Version_VersionType // Human readable version number, e.g. 1.0.3. This is set only when type is // NORMAL. Tsunami uses raw string to represent a version number instead of // any structured messages in order to handle different kinds of version // schemes. Tsunami will tokenize this version string and store tokens // internally. When performing version comparisons, Tsunami follows the // precedence defined by Semantic Versioning (semver.org). More details can be // found in Tsunami's internal Version class. FullVersionString string } func (b0 Version_builder) Build() *Version { m0 := &Version{} b, x := &b0, m0 _, _ = b, x x.xxx_hidden_Type = b.Type x.xxx_hidden_FullVersionString = b.FullVersionString return m0 } // An inclusive range of versions for a software. type VersionRange struct { state protoimpl.MessageState `protogen:"opaque.v1"` xxx_hidden_MinVersion *Version `protobuf:"bytes,1,opt,name=min_version,json=minVersion,proto3"` xxx_hidden_MinVersionInclusiveness VersionRange_Inclusiveness `protobuf:"varint,2,opt,name=min_version_inclusiveness,json=minVersionInclusiveness,proto3,enum=tsunami.proto.VersionRange_Inclusiveness"` xxx_hidden_MaxVersion *Version `protobuf:"bytes,3,opt,name=max_version,json=maxVersion,proto3"` xxx_hidden_MaxVersionInclusiveness VersionRange_Inclusiveness `protobuf:"varint,4,opt,name=max_version_inclusiveness,json=maxVersionInclusiveness,proto3,enum=tsunami.proto.VersionRange_Inclusiveness"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *VersionRange) Reset() { *x = VersionRange{} mi := &file_software_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *VersionRange) String() string { return protoimpl.X.MessageStringOf(x) } func (*VersionRange) ProtoMessage() {} func (x *VersionRange) ProtoReflect() protoreflect.Message { mi := &file_software_proto_msgTypes[1] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } func (x *VersionRange) GetMinVersion() *Version { if x != nil { return x.xxx_hidden_MinVersion } return nil } func (x *VersionRange) GetMinVersionInclusiveness() VersionRange_Inclusiveness { if x != nil { return x.xxx_hidden_MinVersionInclusiveness } return VersionRange_INCLUSIVENESS_UNSPECIFIED } func (x *VersionRange) GetMaxVersion() *Version { if x != nil { return x.xxx_hidden_MaxVersion } return nil } func (x *VersionRange) GetMaxVersionInclusiveness() VersionRange_Inclusiveness { if x != nil { return x.xxx_hidden_MaxVersionInclusiveness } return VersionRange_INCLUSIVENESS_UNSPECIFIED } func (x *VersionRange) SetMinVersion(v *Version) { x.xxx_hidden_MinVersion = v } func (x *VersionRange) SetMinVersionInclusiveness(v VersionRange_Inclusiveness) { x.xxx_hidden_MinVersionInclusiveness = v } func (x *VersionRange) SetMaxVersion(v *Version) { x.xxx_hidden_MaxVersion = v } func (x *VersionRange) SetMaxVersionInclusiveness(v VersionRange_Inclusiveness) { x.xxx_hidden_MaxVersionInclusiveness = v } func (x *VersionRange) HasMinVersion() bool { if x == nil { return false } return x.xxx_hidden_MinVersion != nil } func (x *VersionRange) HasMaxVersion() bool { if x == nil { return false } return x.xxx_hidden_MaxVersion != nil } func (x *VersionRange) ClearMinVersion() { x.xxx_hidden_MinVersion = nil } func (x *VersionRange) ClearMaxVersion() { x.xxx_hidden_MaxVersion = nil } type VersionRange_builder struct { _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. // Minimum version that belongs in the range. MinVersion *Version // Inclusiveness of the min_version. When min_version points to negative // infinity, this value will always be EXCLUSIVE to matching the // representation of (-inf, 1.0]. Note that negative infinity version should // ***NOT*** be compared with a version range as it is just a bogus sentinel // version without any meaning. MinVersionInclusiveness VersionRange_Inclusiveness // Maximum version that belongs in the range. MaxVersion *Version // Inclusiveness of the max_version. When max_version points to positive // infinity, this value will always be EXCLUSIVE to matching the // representation of [1.0, inf). Note that positive infinity version should // ***NOT*** be compared with a version range as it is just a bogus sentinel // version without any meaning. MaxVersionInclusiveness VersionRange_Inclusiveness } func (b0 VersionRange_builder) Build() *VersionRange { m0 := &VersionRange{} b, x := &b0, m0 _, _ = b, x x.xxx_hidden_MinVersion = b.MinVersion x.xxx_hidden_MinVersionInclusiveness = b.MinVersionInclusiveness x.xxx_hidden_MaxVersion = b.MaxVersion x.xxx_hidden_MaxVersionInclusiveness = b.MaxVersionInclusiveness return m0 } // A set of Versions and VersionRanges that completely describes a set of // software releases, e.g. {3.9.1, 3.9.3, [4.7.1, 4.7.8], 4.8} type VersionSet struct { state protoimpl.MessageState `protogen:"opaque.v1"` xxx_hidden_Versions *[]*Version `protobuf:"bytes,1,rep,name=versions,proto3"` xxx_hidden_VersionRanges *[]*VersionRange `protobuf:"bytes,2,rep,name=version_ranges,json=versionRanges,proto3"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *VersionSet) Reset() { *x = VersionSet{} mi := &file_software_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *VersionSet) String() string { return protoimpl.X.MessageStringOf(x) } func (*VersionSet) ProtoMessage() {} func (x *VersionSet) ProtoReflect() protoreflect.Message { mi := &file_software_proto_msgTypes[2] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } func (x *VersionSet) GetVersions() []*Version { if x != nil { if x.xxx_hidden_Versions != nil { return *x.xxx_hidden_Versions } } return nil } func (x *VersionSet) GetVersionRanges() []*VersionRange { if x != nil { if x.xxx_hidden_VersionRanges != nil { return *x.xxx_hidden_VersionRanges } } return nil } func (x *VersionSet) SetVersions(v []*Version) { x.xxx_hidden_Versions = &v } func (x *VersionSet) SetVersionRanges(v []*VersionRange) { x.xxx_hidden_VersionRanges = &v } type VersionSet_builder struct { _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. Versions []*Version VersionRanges []*VersionRange } func (b0 VersionSet_builder) Build() *VersionSet { m0 := &VersionSet{} b, x := &b0, m0 _, _ = b, x x.xxx_hidden_Versions = &b.Versions x.xxx_hidden_VersionRanges = &b.VersionRanges return m0 } // A structured description about a software. type Software struct { state protoimpl.MessageState `protogen:"opaque.v1"` xxx_hidden_Name string `protobuf:"bytes,1,opt,name=name,proto3"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *Software) Reset() { *x = Software{} mi := &file_software_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *Software) String() string { return protoimpl.X.MessageStringOf(x) } func (*Software) ProtoMessage() {} func (x *Software) ProtoReflect() protoreflect.Message { mi := &file_software_proto_msgTypes[3] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } func (x *Software) GetName() string { if x != nil { return x.xxx_hidden_Name } return "" } func (x *Software) SetName(v string) { x.xxx_hidden_Name = v } type Software_builder struct { _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. // The name of this software. Name string } func (b0 Software_builder) Build() *Software { m0 := &Software{} b, x := &b0, m0 _, _ = b, x x.xxx_hidden_Name = b.Name return m0 } var File_software_proto protoreflect.FileDescriptor const file_software_proto_rawDesc = "" + "\n" + "\x0esoftware.proto\x12\rtsunami.proto\"\xc4\x01\n" + "\aVersion\x126\n" + "\x04type\x18\x01 \x01(\x0e2\".tsunami.proto.Version.VersionTypeR\x04type\x12.\n" + "\x13full_version_string\x18\x02 \x01(\tR\x11fullVersionString\"Q\n" + "\vVersionType\x12\x1c\n" + "\x18VERSION_TYPE_UNSPECIFIED\x10\x00\x12\n" + "\n" + "\x06NORMAL\x10\x01\x12\v\n" + "\aMINIMUM\x10\x02\x12\v\n" + "\aMAXIMUM\x10\x03\"\x9c\x03\n" + "\fVersionRange\x127\n" + "\vmin_version\x18\x01 \x01(\v2\x16.tsunami.proto.VersionR\n" + "minVersion\x12e\n" + "\x19min_version_inclusiveness\x18\x02 \x01(\x0e2).tsunami.proto.VersionRange.InclusivenessR\x17minVersionInclusiveness\x127\n" + "\vmax_version\x18\x03 \x01(\v2\x16.tsunami.proto.VersionR\n" + "maxVersion\x12e\n" + "\x19max_version_inclusiveness\x18\x04 \x01(\x0e2).tsunami.proto.VersionRange.InclusivenessR\x17maxVersionInclusiveness\"L\n" + "\rInclusiveness\x12\x1d\n" + "\x19INCLUSIVENESS_UNSPECIFIED\x10\x00\x12\r\n" + "\tINCLUSIVE\x10\x01\x12\r\n" + "\tEXCLUSIVE\x10\x02\"\x84\x01\n" + "\n" + "VersionSet\x122\n" + "\bversions\x18\x01 \x03(\v2\x16.tsunami.proto.VersionR\bversions\x12B\n" + "\x0eversion_ranges\x18\x02 \x03(\v2\x1b.tsunami.proto.VersionRangeR\rversionRanges\"\x1e\n" + "\bSoftware\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04nameBs\n" + "\x18com.google.tsunami.protoB\x0eSoftwareProtosP\x01ZEgithub.com/google/tsunami-security-scanner/proto/go/software_go_protob\x06proto3" var file_software_proto_enumTypes = make([]protoimpl.EnumInfo, 2) var file_software_proto_msgTypes = make([]protoimpl.MessageInfo, 4) var file_software_proto_goTypes = []any{ (Version_VersionType)(0), // 0: tsunami.proto.Version.VersionType (VersionRange_Inclusiveness)(0), // 1: tsunami.proto.VersionRange.Inclusiveness (*Version)(nil), // 2: tsunami.proto.Version (*VersionRange)(nil), // 3: tsunami.proto.VersionRange (*VersionSet)(nil), // 4: tsunami.proto.VersionSet (*Software)(nil), // 5: tsunami.proto.Software } var file_software_proto_depIdxs = []int32{ 0, // 0: tsunami.proto.Version.type:type_name -> tsunami.proto.Version.VersionType 2, // 1: tsunami.proto.VersionRange.min_version:type_name -> tsunami.proto.Version 1, // 2: tsunami.proto.VersionRange.min_version_inclusiveness:type_name -> tsunami.proto.VersionRange.Inclusiveness 2, // 3: tsunami.proto.VersionRange.max_version:type_name -> tsunami.proto.Version 1, // 4: tsunami.proto.VersionRange.max_version_inclusiveness:type_name -> tsunami.proto.VersionRange.Inclusiveness 2, // 5: tsunami.proto.VersionSet.versions:type_name -> tsunami.proto.Version 3, // 6: tsunami.proto.VersionSet.version_ranges:type_name -> tsunami.proto.VersionRange 7, // [7:7] is the sub-list for method output_type 7, // [7:7] is the sub-list for method input_type 7, // [7:7] is the sub-list for extension type_name 7, // [7:7] is the sub-list for extension extendee 0, // [0:7] is the sub-list for field type_name } func init() { file_software_proto_init() } func file_software_proto_init() { if File_software_proto != nil { return } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_software_proto_rawDesc), len(file_software_proto_rawDesc)), NumEnums: 2, NumMessages: 4, NumExtensions: 0, NumServices: 0, }, GoTypes: file_software_proto_goTypes, DependencyIndexes: file_software_proto_depIdxs, EnumInfos: file_software_proto_enumTypes, MessageInfos: file_software_proto_msgTypes, }.Build() File_software_proto = out.File file_software_proto_goTypes = nil file_software_proto_depIdxs = nil } ================================================ FILE: proto/go/vulnerability_go_proto/vulnerability.pb.go ================================================ // // Copyright 2020 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Data models for describing a vulnerability. // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.11 // protoc v3.21.12 // source: vulnerability.proto package vulnerability_go_proto import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" unsafe "unsafe" ) const ( // Verify that this generated code is sufficiently up-to-date. _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) // Verify that runtime/protoimpl is sufficiently up-to-date. _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) // Severity of a vulnerability. type Severity int32 const ( // Unspecified severity. Severity_SEVERITY_UNSPECIFIED Severity = 0 // Minimal severity. Severity_MINIMAL Severity = 1 // Low severity. Severity_LOW Severity = 2 // Medium severity. Severity_MEDIUM Severity = 3 // High severity. Severity_HIGH Severity = 4 // Critical severity. Severity_CRITICAL Severity = 5 ) // Enum value maps for Severity. var ( Severity_name = map[int32]string{ 0: "SEVERITY_UNSPECIFIED", 1: "MINIMAL", 2: "LOW", 3: "MEDIUM", 4: "HIGH", 5: "CRITICAL", } Severity_value = map[string]int32{ "SEVERITY_UNSPECIFIED": 0, "MINIMAL": 1, "LOW": 2, "MEDIUM": 3, "HIGH": 4, "CRITICAL": 5, } ) func (x Severity) Enum() *Severity { p := new(Severity) *p = x return p } func (x Severity) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (Severity) Descriptor() protoreflect.EnumDescriptor { return file_vulnerability_proto_enumTypes[0].Descriptor() } func (Severity) Type() protoreflect.EnumType { return &file_vulnerability_proto_enumTypes[0] } func (x Severity) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } // The identifier that uniquely identifies this vulnerability. type VulnerabilityId struct { state protoimpl.MessageState `protogen:"opaque.v1"` xxx_hidden_Publisher string `protobuf:"bytes,1,opt,name=publisher,proto3"` xxx_hidden_Value string `protobuf:"bytes,2,opt,name=value,proto3"` xxx_hidden_Link string `protobuf:"bytes,3,opt,name=link,proto3"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *VulnerabilityId) Reset() { *x = VulnerabilityId{} mi := &file_vulnerability_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *VulnerabilityId) String() string { return protoimpl.X.MessageStringOf(x) } func (*VulnerabilityId) ProtoMessage() {} func (x *VulnerabilityId) ProtoReflect() protoreflect.Message { mi := &file_vulnerability_proto_msgTypes[0] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } func (x *VulnerabilityId) GetPublisher() string { if x != nil { return x.xxx_hidden_Publisher } return "" } func (x *VulnerabilityId) GetValue() string { if x != nil { return x.xxx_hidden_Value } return "" } func (x *VulnerabilityId) GetLink() string { if x != nil { return x.xxx_hidden_Link } return "" } func (x *VulnerabilityId) SetPublisher(v string) { x.xxx_hidden_Publisher = v } func (x *VulnerabilityId) SetValue(v string) { x.xxx_hidden_Value = v } func (x *VulnerabilityId) SetLink(v string) { x.xxx_hidden_Link = v } type VulnerabilityId_builder struct { _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. // Entity that published this identifier. Publisher string // Publisher assigned unique identifier. Value string // Optional. URL for details about this vulnerability. Link string } func (b0 VulnerabilityId_builder) Build() *VulnerabilityId { m0 := &VulnerabilityId{} b, x := &b0, m0 _, _ = b, x x.xxx_hidden_Publisher = b.Publisher x.xxx_hidden_Value = b.Value x.xxx_hidden_Link = b.Link return m0 } // Message that represents one single vulnerability detected by Tsunami. type Vulnerability struct { state protoimpl.MessageState `protogen:"opaque.v1"` xxx_hidden_MainId *VulnerabilityId `protobuf:"bytes,1,opt,name=main_id,json=mainId,proto3"` xxx_hidden_RelatedId *[]*VulnerabilityId `protobuf:"bytes,2,rep,name=related_id,json=relatedId,proto3"` xxx_hidden_Severity Severity `protobuf:"varint,3,opt,name=severity,proto3,enum=tsunami.proto.Severity"` xxx_hidden_Title string `protobuf:"bytes,4,opt,name=title,proto3"` xxx_hidden_Description string `protobuf:"bytes,5,opt,name=description,proto3"` xxx_hidden_Recommendation string `protobuf:"bytes,6,opt,name=recommendation,proto3"` xxx_hidden_CvssV2 string `protobuf:"bytes,7,opt,name=cvss_v2,json=cvssV2,proto3"` xxx_hidden_CvssV3 string `protobuf:"bytes,8,opt,name=cvss_v3,json=cvssV3,proto3"` xxx_hidden_AdditionalDetails *[]*AdditionalDetail `protobuf:"bytes,9,rep,name=additional_details,json=additionalDetails,proto3"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *Vulnerability) Reset() { *x = Vulnerability{} mi := &file_vulnerability_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *Vulnerability) String() string { return protoimpl.X.MessageStringOf(x) } func (*Vulnerability) ProtoMessage() {} func (x *Vulnerability) ProtoReflect() protoreflect.Message { mi := &file_vulnerability_proto_msgTypes[1] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } func (x *Vulnerability) GetMainId() *VulnerabilityId { if x != nil { return x.xxx_hidden_MainId } return nil } func (x *Vulnerability) GetRelatedId() []*VulnerabilityId { if x != nil { if x.xxx_hidden_RelatedId != nil { return *x.xxx_hidden_RelatedId } } return nil } func (x *Vulnerability) GetSeverity() Severity { if x != nil { return x.xxx_hidden_Severity } return Severity_SEVERITY_UNSPECIFIED } func (x *Vulnerability) GetTitle() string { if x != nil { return x.xxx_hidden_Title } return "" } func (x *Vulnerability) GetDescription() string { if x != nil { return x.xxx_hidden_Description } return "" } func (x *Vulnerability) GetRecommendation() string { if x != nil { return x.xxx_hidden_Recommendation } return "" } func (x *Vulnerability) GetCvssV2() string { if x != nil { return x.xxx_hidden_CvssV2 } return "" } func (x *Vulnerability) GetCvssV3() string { if x != nil { return x.xxx_hidden_CvssV3 } return "" } func (x *Vulnerability) GetAdditionalDetails() []*AdditionalDetail { if x != nil { if x.xxx_hidden_AdditionalDetails != nil { return *x.xxx_hidden_AdditionalDetails } } return nil } func (x *Vulnerability) SetMainId(v *VulnerabilityId) { x.xxx_hidden_MainId = v } func (x *Vulnerability) SetRelatedId(v []*VulnerabilityId) { x.xxx_hidden_RelatedId = &v } func (x *Vulnerability) SetSeverity(v Severity) { x.xxx_hidden_Severity = v } func (x *Vulnerability) SetTitle(v string) { x.xxx_hidden_Title = v } func (x *Vulnerability) SetDescription(v string) { x.xxx_hidden_Description = v } func (x *Vulnerability) SetRecommendation(v string) { x.xxx_hidden_Recommendation = v } func (x *Vulnerability) SetCvssV2(v string) { x.xxx_hidden_CvssV2 = v } func (x *Vulnerability) SetCvssV3(v string) { x.xxx_hidden_CvssV3 = v } func (x *Vulnerability) SetAdditionalDetails(v []*AdditionalDetail) { x.xxx_hidden_AdditionalDetails = &v } func (x *Vulnerability) HasMainId() bool { if x == nil { return false } return x.xxx_hidden_MainId != nil } func (x *Vulnerability) ClearMainId() { x.xxx_hidden_MainId = nil } type Vulnerability_builder struct { _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. // The main identifier for this vulnerability, usually a publicly known // identifier like CVEs and such. If not publicly known, users are expected to // assign an id on their own. MainId *VulnerabilityId // Any related identifiers about this vulnerability, e.g. a CWE weakness. RelatedId []*VulnerabilityId // Severity of this vulnerability. Severity Severity // Terse but descriptive sentence about this vulnerability. // For example: "Default Password (0p3nm35h) for 'root' Account.". Title string // Verbose description of this vulnerability. Description string // Optional. Verbose recommended solution(s). Recommendation string // Optional. The CVSS v2 score of this vulnerability. CvssV2 string // Optional. The CVSS v3 score of this vulnerability. CvssV3 string // Any additional technical details about this vulnerability. AdditionalDetails []*AdditionalDetail } func (b0 Vulnerability_builder) Build() *Vulnerability { m0 := &Vulnerability{} b, x := &b0, m0 _, _ = b, x x.xxx_hidden_MainId = b.MainId x.xxx_hidden_RelatedId = &b.RelatedId x.xxx_hidden_Severity = b.Severity x.xxx_hidden_Title = b.Title x.xxx_hidden_Description = b.Description x.xxx_hidden_Recommendation = b.Recommendation x.xxx_hidden_CvssV2 = b.CvssV2 x.xxx_hidden_CvssV3 = b.CvssV3 x.xxx_hidden_AdditionalDetails = &b.AdditionalDetails return m0 } // A list of vulnerabilities. Used mostly for export purposes. type VulnerabilityList struct { state protoimpl.MessageState `protogen:"opaque.v1"` xxx_hidden_Vulnerabilities *[]*Vulnerability `protobuf:"bytes,1,rep,name=vulnerabilities,proto3"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *VulnerabilityList) Reset() { *x = VulnerabilityList{} mi := &file_vulnerability_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *VulnerabilityList) String() string { return protoimpl.X.MessageStringOf(x) } func (*VulnerabilityList) ProtoMessage() {} func (x *VulnerabilityList) ProtoReflect() protoreflect.Message { mi := &file_vulnerability_proto_msgTypes[2] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } func (x *VulnerabilityList) GetVulnerabilities() []*Vulnerability { if x != nil { if x.xxx_hidden_Vulnerabilities != nil { return *x.xxx_hidden_Vulnerabilities } } return nil } func (x *VulnerabilityList) SetVulnerabilities(v []*Vulnerability) { x.xxx_hidden_Vulnerabilities = &v } type VulnerabilityList_builder struct { _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. Vulnerabilities []*Vulnerability } func (b0 VulnerabilityList_builder) Build() *VulnerabilityList { m0 := &VulnerabilityList{} b, x := &b0, m0 _, _ = b, x x.xxx_hidden_Vulnerabilities = &b.Vulnerabilities return m0 } // Additional details regarding a vulnerability can be stored here. Prefers to // use the existing structured data when possible, otherwise store the raw data // as a blob. type AdditionalDetail struct { state protoimpl.MessageState `protogen:"opaque.v1"` xxx_hidden_Description string `protobuf:"bytes,1,opt,name=description,proto3"` xxx_hidden_Detail isAdditionalDetail_Detail `protobuf_oneof:"detail"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *AdditionalDetail) Reset() { *x = AdditionalDetail{} mi := &file_vulnerability_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *AdditionalDetail) String() string { return protoimpl.X.MessageStringOf(x) } func (*AdditionalDetail) ProtoMessage() {} func (x *AdditionalDetail) ProtoReflect() protoreflect.Message { mi := &file_vulnerability_proto_msgTypes[3] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } func (x *AdditionalDetail) GetDescription() string { if x != nil { return x.xxx_hidden_Description } return "" } func (x *AdditionalDetail) GetBlobData() *BlobData { if x != nil { if x, ok := x.xxx_hidden_Detail.(*additionalDetail_BlobData); ok { return x.BlobData } } return nil } func (x *AdditionalDetail) GetTextData() *TextData { if x != nil { if x, ok := x.xxx_hidden_Detail.(*additionalDetail_TextData); ok { return x.TextData } } return nil } func (x *AdditionalDetail) GetCredential() *Credential { if x != nil { if x, ok := x.xxx_hidden_Detail.(*additionalDetail_Credential); ok { return x.Credential } } return nil } func (x *AdditionalDetail) GetCredentials() *Credentials { if x != nil { if x, ok := x.xxx_hidden_Detail.(*additionalDetail_Credentials); ok { return x.Credentials } } return nil } func (x *AdditionalDetail) SetDescription(v string) { x.xxx_hidden_Description = v } func (x *AdditionalDetail) SetBlobData(v *BlobData) { if v == nil { x.xxx_hidden_Detail = nil return } x.xxx_hidden_Detail = &additionalDetail_BlobData{v} } func (x *AdditionalDetail) SetTextData(v *TextData) { if v == nil { x.xxx_hidden_Detail = nil return } x.xxx_hidden_Detail = &additionalDetail_TextData{v} } func (x *AdditionalDetail) SetCredential(v *Credential) { if v == nil { x.xxx_hidden_Detail = nil return } x.xxx_hidden_Detail = &additionalDetail_Credential{v} } func (x *AdditionalDetail) SetCredentials(v *Credentials) { if v == nil { x.xxx_hidden_Detail = nil return } x.xxx_hidden_Detail = &additionalDetail_Credentials{v} } func (x *AdditionalDetail) HasDetail() bool { if x == nil { return false } return x.xxx_hidden_Detail != nil } func (x *AdditionalDetail) HasBlobData() bool { if x == nil { return false } _, ok := x.xxx_hidden_Detail.(*additionalDetail_BlobData) return ok } func (x *AdditionalDetail) HasTextData() bool { if x == nil { return false } _, ok := x.xxx_hidden_Detail.(*additionalDetail_TextData) return ok } func (x *AdditionalDetail) HasCredential() bool { if x == nil { return false } _, ok := x.xxx_hidden_Detail.(*additionalDetail_Credential) return ok } func (x *AdditionalDetail) HasCredentials() bool { if x == nil { return false } _, ok := x.xxx_hidden_Detail.(*additionalDetail_Credentials) return ok } func (x *AdditionalDetail) ClearDetail() { x.xxx_hidden_Detail = nil } func (x *AdditionalDetail) ClearBlobData() { if _, ok := x.xxx_hidden_Detail.(*additionalDetail_BlobData); ok { x.xxx_hidden_Detail = nil } } func (x *AdditionalDetail) ClearTextData() { if _, ok := x.xxx_hidden_Detail.(*additionalDetail_TextData); ok { x.xxx_hidden_Detail = nil } } func (x *AdditionalDetail) ClearCredential() { if _, ok := x.xxx_hidden_Detail.(*additionalDetail_Credential); ok { x.xxx_hidden_Detail = nil } } func (x *AdditionalDetail) ClearCredentials() { if _, ok := x.xxx_hidden_Detail.(*additionalDetail_Credentials); ok { x.xxx_hidden_Detail = nil } } const AdditionalDetail_Detail_not_set_case case_AdditionalDetail_Detail = 0 const AdditionalDetail_BlobData_case case_AdditionalDetail_Detail = 2 const AdditionalDetail_TextData_case case_AdditionalDetail_Detail = 3 const AdditionalDetail_Credential_case case_AdditionalDetail_Detail = 4 const AdditionalDetail_Credentials_case case_AdditionalDetail_Detail = 5 func (x *AdditionalDetail) WhichDetail() case_AdditionalDetail_Detail { if x == nil { return AdditionalDetail_Detail_not_set_case } switch x.xxx_hidden_Detail.(type) { case *additionalDetail_BlobData: return AdditionalDetail_BlobData_case case *additionalDetail_TextData: return AdditionalDetail_TextData_case case *additionalDetail_Credential: return AdditionalDetail_Credential_case case *additionalDetail_Credentials: return AdditionalDetail_Credentials_case default: return AdditionalDetail_Detail_not_set_case } } type AdditionalDetail_builder struct { _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. Description string // Fields of oneof xxx_hidden_Detail: BlobData *BlobData TextData *TextData Credential *Credential Credentials *Credentials // -- end of xxx_hidden_Detail } func (b0 AdditionalDetail_builder) Build() *AdditionalDetail { m0 := &AdditionalDetail{} b, x := &b0, m0 _, _ = b, x x.xxx_hidden_Description = b.Description if b.BlobData != nil { x.xxx_hidden_Detail = &additionalDetail_BlobData{b.BlobData} } if b.TextData != nil { x.xxx_hidden_Detail = &additionalDetail_TextData{b.TextData} } if b.Credential != nil { x.xxx_hidden_Detail = &additionalDetail_Credential{b.Credential} } if b.Credentials != nil { x.xxx_hidden_Detail = &additionalDetail_Credentials{b.Credentials} } return m0 } type case_AdditionalDetail_Detail protoreflect.FieldNumber func (x case_AdditionalDetail_Detail) String() string { md := file_vulnerability_proto_msgTypes[3].Descriptor() if x == 0 { return "not set" } return protoimpl.X.MessageFieldStringOf(md, protoreflect.FieldNumber(x)) } type isAdditionalDetail_Detail interface { isAdditionalDetail_Detail() } type additionalDetail_BlobData struct { BlobData *BlobData `protobuf:"bytes,2,opt,name=blob_data,json=blobData,proto3,oneof"` } type additionalDetail_TextData struct { TextData *TextData `protobuf:"bytes,3,opt,name=text_data,json=textData,proto3,oneof"` } type additionalDetail_Credential struct { Credential *Credential `protobuf:"bytes,4,opt,name=credential,proto3,oneof"` } type additionalDetail_Credentials struct { Credentials *Credentials `protobuf:"bytes,5,opt,name=credentials,proto3,oneof"` } func (*additionalDetail_BlobData) isAdditionalDetail_Detail() {} func (*additionalDetail_TextData) isAdditionalDetail_Detail() {} func (*additionalDetail_Credential) isAdditionalDetail_Detail() {} func (*additionalDetail_Credentials) isAdditionalDetail_Detail() {} // A piece of arbitrary binary data. type BlobData struct { state protoimpl.MessageState `protogen:"opaque.v1"` xxx_hidden_Data []byte `protobuf:"bytes,1,opt,name=data,proto3"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *BlobData) Reset() { *x = BlobData{} mi := &file_vulnerability_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *BlobData) String() string { return protoimpl.X.MessageStringOf(x) } func (*BlobData) ProtoMessage() {} func (x *BlobData) ProtoReflect() protoreflect.Message { mi := &file_vulnerability_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } func (x *BlobData) GetData() []byte { if x != nil { return x.xxx_hidden_Data } return nil } func (x *BlobData) SetData(v []byte) { if v == nil { v = []byte{} } x.xxx_hidden_Data = v } type BlobData_builder struct { _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. Data []byte } func (b0 BlobData_builder) Build() *BlobData { m0 := &BlobData{} b, x := &b0, m0 _, _ = b, x x.xxx_hidden_Data = b.Data return m0 } // A piece of arbitrary UTF-8 encoded text data. type TextData struct { state protoimpl.MessageState `protogen:"opaque.v1"` xxx_hidden_Text string `protobuf:"bytes,1,opt,name=text,proto3"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *TextData) Reset() { *x = TextData{} mi := &file_vulnerability_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *TextData) String() string { return protoimpl.X.MessageStringOf(x) } func (*TextData) ProtoMessage() {} func (x *TextData) ProtoReflect() protoreflect.Message { mi := &file_vulnerability_proto_msgTypes[5] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } func (x *TextData) GetText() string { if x != nil { return x.xxx_hidden_Text } return "" } func (x *TextData) SetText(v string) { x.xxx_hidden_Text = v } type TextData_builder struct { _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. Text string } func (b0 TextData_builder) Build() *TextData { m0 := &TextData{} b, x := &b0, m0 _, _ = b, x x.xxx_hidden_Text = b.Text return m0 } // Credential for a vulnerable network service. type Credential struct { state protoimpl.MessageState `protogen:"opaque.v1"` xxx_hidden_Username string `protobuf:"bytes,1,opt,name=username,proto3"` xxx_hidden_Password string `protobuf:"bytes,2,opt,name=password,proto3"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *Credential) Reset() { *x = Credential{} mi := &file_vulnerability_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *Credential) String() string { return protoimpl.X.MessageStringOf(x) } func (*Credential) ProtoMessage() {} func (x *Credential) ProtoReflect() protoreflect.Message { mi := &file_vulnerability_proto_msgTypes[6] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } func (x *Credential) GetUsername() string { if x != nil { return x.xxx_hidden_Username } return "" } func (x *Credential) GetPassword() string { if x != nil { return x.xxx_hidden_Password } return "" } func (x *Credential) SetUsername(v string) { x.xxx_hidden_Username = v } func (x *Credential) SetPassword(v string) { x.xxx_hidden_Password = v } type Credential_builder struct { _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. Username string Password string } func (b0 Credential_builder) Build() *Credential { m0 := &Credential{} b, x := &b0, m0 _, _ = b, x x.xxx_hidden_Username = b.Username x.xxx_hidden_Password = b.Password return m0 } // A set of credentials for a vulnerable network service. type Credentials struct { state protoimpl.MessageState `protogen:"opaque.v1"` xxx_hidden_Credential *[]*Credential `protobuf:"bytes,1,rep,name=credential,proto3"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *Credentials) Reset() { *x = Credentials{} mi := &file_vulnerability_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *Credentials) String() string { return protoimpl.X.MessageStringOf(x) } func (*Credentials) ProtoMessage() {} func (x *Credentials) ProtoReflect() protoreflect.Message { mi := &file_vulnerability_proto_msgTypes[7] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } func (x *Credentials) GetCredential() []*Credential { if x != nil { if x.xxx_hidden_Credential != nil { return *x.xxx_hidden_Credential } } return nil } func (x *Credentials) SetCredential(v []*Credential) { x.xxx_hidden_Credential = &v } type Credentials_builder struct { _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. Credential []*Credential } func (b0 Credentials_builder) Build() *Credentials { m0 := &Credentials{} b, x := &b0, m0 _, _ = b, x x.xxx_hidden_Credential = &b.Credential return m0 } var File_vulnerability_proto protoreflect.FileDescriptor const file_vulnerability_proto_rawDesc = "" + "\n" + "\x13vulnerability.proto\x12\rtsunami.proto\"Y\n" + "\x0fVulnerabilityId\x12\x1c\n" + "\tpublisher\x18\x01 \x01(\tR\tpublisher\x12\x14\n" + "\x05value\x18\x02 \x01(\tR\x05value\x12\x12\n" + "\x04link\x18\x03 \x01(\tR\x04link\"\x9e\x03\n" + "\rVulnerability\x127\n" + "\amain_id\x18\x01 \x01(\v2\x1e.tsunami.proto.VulnerabilityIdR\x06mainId\x12=\n" + "\n" + "related_id\x18\x02 \x03(\v2\x1e.tsunami.proto.VulnerabilityIdR\trelatedId\x123\n" + "\bseverity\x18\x03 \x01(\x0e2\x17.tsunami.proto.SeverityR\bseverity\x12\x14\n" + "\x05title\x18\x04 \x01(\tR\x05title\x12 \n" + "\vdescription\x18\x05 \x01(\tR\vdescription\x12&\n" + "\x0erecommendation\x18\x06 \x01(\tR\x0erecommendation\x12\x17\n" + "\acvss_v2\x18\a \x01(\tR\x06cvssV2\x12\x17\n" + "\acvss_v3\x18\b \x01(\tR\x06cvssV3\x12N\n" + "\x12additional_details\x18\t \x03(\v2\x1f.tsunami.proto.AdditionalDetailR\x11additionalDetails\"[\n" + "\x11VulnerabilityList\x12F\n" + "\x0fvulnerabilities\x18\x01 \x03(\v2\x1c.tsunami.proto.VulnerabilityR\x0fvulnerabilities\"\xab\x02\n" + "\x10AdditionalDetail\x12 \n" + "\vdescription\x18\x01 \x01(\tR\vdescription\x126\n" + "\tblob_data\x18\x02 \x01(\v2\x17.tsunami.proto.BlobDataH\x00R\bblobData\x126\n" + "\ttext_data\x18\x03 \x01(\v2\x17.tsunami.proto.TextDataH\x00R\btextData\x12;\n" + "\n" + "credential\x18\x04 \x01(\v2\x19.tsunami.proto.CredentialH\x00R\n" + "credential\x12>\n" + "\vcredentials\x18\x05 \x01(\v2\x1a.tsunami.proto.CredentialsH\x00R\vcredentialsB\b\n" + "\x06detail\"\x1e\n" + "\bBlobData\x12\x12\n" + "\x04data\x18\x01 \x01(\fR\x04data\"\x1e\n" + "\bTextData\x12\x12\n" + "\x04text\x18\x01 \x01(\tR\x04text\"D\n" + "\n" + "Credential\x12\x1a\n" + "\busername\x18\x01 \x01(\tR\busername\x12\x1a\n" + "\bpassword\x18\x02 \x01(\tR\bpassword\"H\n" + "\vCredentials\x129\n" + "\n" + "credential\x18\x01 \x03(\v2\x19.tsunami.proto.CredentialR\n" + "credential*^\n" + "\bSeverity\x12\x18\n" + "\x14SEVERITY_UNSPECIFIED\x10\x00\x12\v\n" + "\aMINIMAL\x10\x01\x12\a\n" + "\x03LOW\x10\x02\x12\n" + "\n" + "\x06MEDIUM\x10\x03\x12\b\n" + "\x04HIGH\x10\x04\x12\f\n" + "\bCRITICAL\x10\x05B}\n" + "\x18com.google.tsunami.protoB\x13VulnerabilityProtosP\x01ZJgithub.com/google/tsunami-security-scanner/proto/go/vulnerability_go_protob\x06proto3" var file_vulnerability_proto_enumTypes = make([]protoimpl.EnumInfo, 1) var file_vulnerability_proto_msgTypes = make([]protoimpl.MessageInfo, 8) var file_vulnerability_proto_goTypes = []any{ (Severity)(0), // 0: tsunami.proto.Severity (*VulnerabilityId)(nil), // 1: tsunami.proto.VulnerabilityId (*Vulnerability)(nil), // 2: tsunami.proto.Vulnerability (*VulnerabilityList)(nil), // 3: tsunami.proto.VulnerabilityList (*AdditionalDetail)(nil), // 4: tsunami.proto.AdditionalDetail (*BlobData)(nil), // 5: tsunami.proto.BlobData (*TextData)(nil), // 6: tsunami.proto.TextData (*Credential)(nil), // 7: tsunami.proto.Credential (*Credentials)(nil), // 8: tsunami.proto.Credentials } var file_vulnerability_proto_depIdxs = []int32{ 1, // 0: tsunami.proto.Vulnerability.main_id:type_name -> tsunami.proto.VulnerabilityId 1, // 1: tsunami.proto.Vulnerability.related_id:type_name -> tsunami.proto.VulnerabilityId 0, // 2: tsunami.proto.Vulnerability.severity:type_name -> tsunami.proto.Severity 4, // 3: tsunami.proto.Vulnerability.additional_details:type_name -> tsunami.proto.AdditionalDetail 2, // 4: tsunami.proto.VulnerabilityList.vulnerabilities:type_name -> tsunami.proto.Vulnerability 5, // 5: tsunami.proto.AdditionalDetail.blob_data:type_name -> tsunami.proto.BlobData 6, // 6: tsunami.proto.AdditionalDetail.text_data:type_name -> tsunami.proto.TextData 7, // 7: tsunami.proto.AdditionalDetail.credential:type_name -> tsunami.proto.Credential 8, // 8: tsunami.proto.AdditionalDetail.credentials:type_name -> tsunami.proto.Credentials 7, // 9: tsunami.proto.Credentials.credential:type_name -> tsunami.proto.Credential 10, // [10:10] is the sub-list for method output_type 10, // [10:10] is the sub-list for method input_type 10, // [10:10] is the sub-list for extension type_name 10, // [10:10] is the sub-list for extension extendee 0, // [0:10] is the sub-list for field type_name } func init() { file_vulnerability_proto_init() } func file_vulnerability_proto_init() { if File_vulnerability_proto != nil { return } file_vulnerability_proto_msgTypes[3].OneofWrappers = []any{ (*additionalDetail_BlobData)(nil), (*additionalDetail_TextData)(nil), (*additionalDetail_Credential)(nil), (*additionalDetail_Credentials)(nil), } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_vulnerability_proto_rawDesc), len(file_vulnerability_proto_rawDesc)), NumEnums: 1, NumMessages: 8, NumExtensions: 0, NumServices: 0, }, GoTypes: file_vulnerability_proto_goTypes, DependencyIndexes: file_vulnerability_proto_depIdxs, EnumInfos: file_vulnerability_proto_enumTypes, MessageInfos: file_vulnerability_proto_msgTypes, }.Build() File_vulnerability_proto = out.File file_vulnerability_proto_goTypes = nil file_vulnerability_proto_depIdxs = nil } ================================================ FILE: proto/go/web_crawl_go_proto/web_crawl.pb.go ================================================ // // Copyright 2020 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Data models for the web crawler. // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.11 // protoc v3.21.12 // source: web_crawl.proto package web_crawl_go_proto import ( network_go_proto "github.com/google/tsunami-security-scanner/proto/go/network_go_proto" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" unsafe "unsafe" ) const ( // Verify that this generated code is sufficiently up-to-date. _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) // Verify that runtime/protoimpl is sufficiently up-to-date. _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) // The type of content stored in the CrawlResult. type CrawlContentType int32 const ( CrawlContentType_CONTENT_TYPE_UNSPECIFIED CrawlContentType = 0 CrawlContentType_CONTENT_TYPE_RAW CrawlContentType = 1 CrawlContentType_CONTENT_TYPE_HASH CrawlContentType = 2 ) // Enum value maps for CrawlContentType. var ( CrawlContentType_name = map[int32]string{ 0: "CONTENT_TYPE_UNSPECIFIED", 1: "CONTENT_TYPE_RAW", 2: "CONTENT_TYPE_HASH", } CrawlContentType_value = map[string]int32{ "CONTENT_TYPE_UNSPECIFIED": 0, "CONTENT_TYPE_RAW": 1, "CONTENT_TYPE_HASH": 2, } ) func (x CrawlContentType) Enum() *CrawlContentType { p := new(CrawlContentType) *p = x return p } func (x CrawlContentType) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (CrawlContentType) Descriptor() protoreflect.EnumDescriptor { return file_web_crawl_proto_enumTypes[0].Descriptor() } func (CrawlContentType) Type() protoreflect.EnumType { return &file_web_crawl_proto_enumTypes[0] } func (x CrawlContentType) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } // Next ID: 7 type CrawlConfig struct { state protoimpl.MessageState `protogen:"opaque.v1"` xxx_hidden_SeedingUrls []string `protobuf:"bytes,1,rep,name=seeding_urls,json=seedingUrls,proto3"` xxx_hidden_MaxDepth int32 `protobuf:"varint,2,opt,name=max_depth,json=maxDepth,proto3"` xxx_hidden_Scopes *[]*CrawlConfig_Scope `protobuf:"bytes,3,rep,name=scopes,proto3"` xxx_hidden_ShouldEnforceScopeCheck bool `protobuf:"varint,5,opt,name=should_enforce_scope_check,json=shouldEnforceScopeCheck,proto3"` xxx_hidden_NetworkEndpoint *network_go_proto.NetworkEndpoint `protobuf:"bytes,6,opt,name=network_endpoint,json=networkEndpoint,proto3"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *CrawlConfig) Reset() { *x = CrawlConfig{} mi := &file_web_crawl_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *CrawlConfig) String() string { return protoimpl.X.MessageStringOf(x) } func (*CrawlConfig) ProtoMessage() {} func (x *CrawlConfig) ProtoReflect() protoreflect.Message { mi := &file_web_crawl_proto_msgTypes[0] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } func (x *CrawlConfig) GetSeedingUrls() []string { if x != nil { return x.xxx_hidden_SeedingUrls } return nil } func (x *CrawlConfig) GetMaxDepth() int32 { if x != nil { return x.xxx_hidden_MaxDepth } return 0 } func (x *CrawlConfig) GetScopes() []*CrawlConfig_Scope { if x != nil { if x.xxx_hidden_Scopes != nil { return *x.xxx_hidden_Scopes } } return nil } func (x *CrawlConfig) GetShouldEnforceScopeCheck() bool { if x != nil { return x.xxx_hidden_ShouldEnforceScopeCheck } return false } func (x *CrawlConfig) GetNetworkEndpoint() *network_go_proto.NetworkEndpoint { if x != nil { return x.xxx_hidden_NetworkEndpoint } return nil } func (x *CrawlConfig) SetSeedingUrls(v []string) { x.xxx_hidden_SeedingUrls = v } func (x *CrawlConfig) SetMaxDepth(v int32) { x.xxx_hidden_MaxDepth = v } func (x *CrawlConfig) SetScopes(v []*CrawlConfig_Scope) { x.xxx_hidden_Scopes = &v } func (x *CrawlConfig) SetShouldEnforceScopeCheck(v bool) { x.xxx_hidden_ShouldEnforceScopeCheck = v } func (x *CrawlConfig) SetNetworkEndpoint(v *network_go_proto.NetworkEndpoint) { x.xxx_hidden_NetworkEndpoint = v } func (x *CrawlConfig) HasNetworkEndpoint() bool { if x == nil { return false } return x.xxx_hidden_NetworkEndpoint != nil } func (x *CrawlConfig) ClearNetworkEndpoint() { x.xxx_hidden_NetworkEndpoint = nil } type CrawlConfig_builder struct { _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. // Starting points of a web crawl. // Required. SeedingUrls []string // The maximum depth of a web crawl. // Required. MaxDepth int32 // Allowed crawling scopes. // Optional. When empty, scopes are autogenerated from seeding_urls. Scopes []*CrawlConfig_Scope // Whether crawling scope check should be enforced. // Optional. ShouldEnforceScopeCheck bool // The network endpoint to be crawled. // Required. NetworkEndpoint *network_go_proto.NetworkEndpoint } func (b0 CrawlConfig_builder) Build() *CrawlConfig { m0 := &CrawlConfig{} b, x := &b0, m0 _, _ = b, x x.xxx_hidden_SeedingUrls = b.SeedingUrls x.xxx_hidden_MaxDepth = b.MaxDepth x.xxx_hidden_Scopes = &b.Scopes x.xxx_hidden_ShouldEnforceScopeCheck = b.ShouldEnforceScopeCheck x.xxx_hidden_NetworkEndpoint = b.NetworkEndpoint return m0 } type CrawlTarget struct { state protoimpl.MessageState `protogen:"opaque.v1"` xxx_hidden_Url string `protobuf:"bytes,1,opt,name=url,proto3"` xxx_hidden_HttpMethod string `protobuf:"bytes,2,opt,name=http_method,json=httpMethod,proto3"` xxx_hidden_HttpRequestBody []byte `protobuf:"bytes,3,opt,name=http_request_body,json=httpRequestBody,proto3"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *CrawlTarget) Reset() { *x = CrawlTarget{} mi := &file_web_crawl_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *CrawlTarget) String() string { return protoimpl.X.MessageStringOf(x) } func (*CrawlTarget) ProtoMessage() {} func (x *CrawlTarget) ProtoReflect() protoreflect.Message { mi := &file_web_crawl_proto_msgTypes[1] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } func (x *CrawlTarget) GetUrl() string { if x != nil { return x.xxx_hidden_Url } return "" } func (x *CrawlTarget) GetHttpMethod() string { if x != nil { return x.xxx_hidden_HttpMethod } return "" } func (x *CrawlTarget) GetHttpRequestBody() []byte { if x != nil { return x.xxx_hidden_HttpRequestBody } return nil } func (x *CrawlTarget) SetUrl(v string) { x.xxx_hidden_Url = v } func (x *CrawlTarget) SetHttpMethod(v string) { x.xxx_hidden_HttpMethod = v } func (x *CrawlTarget) SetHttpRequestBody(v []byte) { if v == nil { v = []byte{} } x.xxx_hidden_HttpRequestBody = v } type CrawlTarget_builder struct { _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. // The URL pointing to the document. Url string // HTTP method to reach the url. Value must be in all upper case, like "GET". HttpMethod string // An optional HTTP request body sent to the crawl URL. HttpRequestBody []byte } func (b0 CrawlTarget_builder) Build() *CrawlTarget { m0 := &CrawlTarget{} b, x := &b0, m0 _, _ = b, x x.xxx_hidden_Url = b.Url x.xxx_hidden_HttpMethod = b.HttpMethod x.xxx_hidden_HttpRequestBody = b.HttpRequestBody return m0 } // Represents an HTTP header. type HttpHeader struct { state protoimpl.MessageState `protogen:"opaque.v1"` xxx_hidden_Key string `protobuf:"bytes,1,opt,name=key,proto3"` xxx_hidden_Value string `protobuf:"bytes,2,opt,name=value,proto3"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *HttpHeader) Reset() { *x = HttpHeader{} mi := &file_web_crawl_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *HttpHeader) String() string { return protoimpl.X.MessageStringOf(x) } func (*HttpHeader) ProtoMessage() {} func (x *HttpHeader) ProtoReflect() protoreflect.Message { mi := &file_web_crawl_proto_msgTypes[2] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } func (x *HttpHeader) GetKey() string { if x != nil { return x.xxx_hidden_Key } return "" } func (x *HttpHeader) GetValue() string { if x != nil { return x.xxx_hidden_Value } return "" } func (x *HttpHeader) SetKey(v string) { x.xxx_hidden_Key = v } func (x *HttpHeader) SetValue(v string) { x.xxx_hidden_Value = v } type HttpHeader_builder struct { _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. Key string Value string } func (b0 HttpHeader_builder) Build() *HttpHeader { m0 := &HttpHeader{} b, x := &b0, m0 _, _ = b, x x.xxx_hidden_Key = b.Key x.xxx_hidden_Value = b.Value return m0 } type CrawlResult struct { state protoimpl.MessageState `protogen:"opaque.v1"` xxx_hidden_CrawlTarget *CrawlTarget `protobuf:"bytes,1,opt,name=crawl_target,json=crawlTarget,proto3"` xxx_hidden_CrawlDepth int32 `protobuf:"varint,2,opt,name=crawl_depth,json=crawlDepth,proto3"` xxx_hidden_ResponseCode int32 `protobuf:"varint,3,opt,name=response_code,json=responseCode,proto3"` xxx_hidden_ContentType string `protobuf:"bytes,4,opt,name=content_type,json=contentType,proto3"` xxx_hidden_Content []byte `protobuf:"bytes,5,opt,name=content,proto3"` xxx_hidden_ResponseHeaders *[]*HttpHeader `protobuf:"bytes,6,rep,name=response_headers,json=responseHeaders,proto3"` xxx_hidden_CrawlContentType CrawlContentType `protobuf:"varint,7,opt,name=crawl_content_type,json=crawlContentType,proto3,enum=tsunami.proto.CrawlContentType"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *CrawlResult) Reset() { *x = CrawlResult{} mi := &file_web_crawl_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *CrawlResult) String() string { return protoimpl.X.MessageStringOf(x) } func (*CrawlResult) ProtoMessage() {} func (x *CrawlResult) ProtoReflect() protoreflect.Message { mi := &file_web_crawl_proto_msgTypes[3] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } func (x *CrawlResult) GetCrawlTarget() *CrawlTarget { if x != nil { return x.xxx_hidden_CrawlTarget } return nil } func (x *CrawlResult) GetCrawlDepth() int32 { if x != nil { return x.xxx_hidden_CrawlDepth } return 0 } func (x *CrawlResult) GetResponseCode() int32 { if x != nil { return x.xxx_hidden_ResponseCode } return 0 } func (x *CrawlResult) GetContentType() string { if x != nil { return x.xxx_hidden_ContentType } return "" } func (x *CrawlResult) GetContent() []byte { if x != nil { return x.xxx_hidden_Content } return nil } func (x *CrawlResult) GetResponseHeaders() []*HttpHeader { if x != nil { if x.xxx_hidden_ResponseHeaders != nil { return *x.xxx_hidden_ResponseHeaders } } return nil } func (x *CrawlResult) GetCrawlContentType() CrawlContentType { if x != nil { return x.xxx_hidden_CrawlContentType } return CrawlContentType_CONTENT_TYPE_UNSPECIFIED } func (x *CrawlResult) SetCrawlTarget(v *CrawlTarget) { x.xxx_hidden_CrawlTarget = v } func (x *CrawlResult) SetCrawlDepth(v int32) { x.xxx_hidden_CrawlDepth = v } func (x *CrawlResult) SetResponseCode(v int32) { x.xxx_hidden_ResponseCode = v } func (x *CrawlResult) SetContentType(v string) { x.xxx_hidden_ContentType = v } func (x *CrawlResult) SetContent(v []byte) { if v == nil { v = []byte{} } x.xxx_hidden_Content = v } func (x *CrawlResult) SetResponseHeaders(v []*HttpHeader) { x.xxx_hidden_ResponseHeaders = &v } func (x *CrawlResult) SetCrawlContentType(v CrawlContentType) { x.xxx_hidden_CrawlContentType = v } func (x *CrawlResult) HasCrawlTarget() bool { if x == nil { return false } return x.xxx_hidden_CrawlTarget != nil } func (x *CrawlResult) ClearCrawlTarget() { x.xxx_hidden_CrawlTarget = nil } type CrawlResult_builder struct { _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. // The target visited by the crawler. CrawlTarget *CrawlTarget // Depth at which the target was visited. CrawlDepth int32 // Response code from the crawled target. ResponseCode int32 // Content type of the resource served at the crawl target. ContentType string // The content of the resource served at the crawl target. Content []byte // Http headers of the response ResponseHeaders []*HttpHeader // The type of content stored in the crawl_results. By default, the whole // response body is stored (RAW). But some configuration can request storing // only a hash of the response body (HASH). CrawlContentType CrawlContentType } func (b0 CrawlResult_builder) Build() *CrawlResult { m0 := &CrawlResult{} b, x := &b0, m0 _, _ = b, x x.xxx_hidden_CrawlTarget = b.CrawlTarget x.xxx_hidden_CrawlDepth = b.CrawlDepth x.xxx_hidden_ResponseCode = b.ResponseCode x.xxx_hidden_ContentType = b.ContentType x.xxx_hidden_Content = b.Content x.xxx_hidden_ResponseHeaders = &b.ResponseHeaders x.xxx_hidden_CrawlContentType = b.CrawlContentType return m0 } // The crawler should only interact with web resources under certain scopes. type CrawlConfig_Scope struct { state protoimpl.MessageState `protogen:"opaque.v1"` xxx_hidden_Domain string `protobuf:"bytes,1,opt,name=domain,proto3"` xxx_hidden_Path string `protobuf:"bytes,2,opt,name=path,proto3"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *CrawlConfig_Scope) Reset() { *x = CrawlConfig_Scope{} mi := &file_web_crawl_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *CrawlConfig_Scope) String() string { return protoimpl.X.MessageStringOf(x) } func (*CrawlConfig_Scope) ProtoMessage() {} func (x *CrawlConfig_Scope) ProtoReflect() protoreflect.Message { mi := &file_web_crawl_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } func (x *CrawlConfig_Scope) GetDomain() string { if x != nil { return x.xxx_hidden_Domain } return "" } func (x *CrawlConfig_Scope) GetPath() string { if x != nil { return x.xxx_hidden_Path } return "" } func (x *CrawlConfig_Scope) SetDomain(v string) { x.xxx_hidden_Domain = v } func (x *CrawlConfig_Scope) SetPath(v string) { x.xxx_hidden_Path = v } type CrawlConfig_Scope_builder struct { _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. // The domain of the scope, only URLs that are on the same domain or a // subdomain will be admitted for crawling. Domain might include a port. // Required. Domain string // The path of the scope, only URLs that are under the same path will be // admitted for crawling. // Optional. When empty, all URLs under the same domain are allowed, // regardless of the paths. Path string } func (b0 CrawlConfig_Scope_builder) Build() *CrawlConfig_Scope { m0 := &CrawlConfig_Scope{} b, x := &b0, m0 _, _ = b, x x.xxx_hidden_Domain = b.Domain x.xxx_hidden_Path = b.Path return m0 } var File_web_crawl_proto protoreflect.FileDescriptor const file_web_crawl_proto_rawDesc = "" + "\n" + "\x0fweb_crawl.proto\x12\rtsunami.proto\x1a\rnetwork.proto\"\xca\x02\n" + "\vCrawlConfig\x12!\n" + "\fseeding_urls\x18\x01 \x03(\tR\vseedingUrls\x12\x1b\n" + "\tmax_depth\x18\x02 \x01(\x05R\bmaxDepth\x128\n" + "\x06scopes\x18\x03 \x03(\v2 .tsunami.proto.CrawlConfig.ScopeR\x06scopes\x12;\n" + "\x1ashould_enforce_scope_check\x18\x05 \x01(\bR\x17shouldEnforceScopeCheck\x12I\n" + "\x10network_endpoint\x18\x06 \x01(\v2\x1e.tsunami.proto.NetworkEndpointR\x0fnetworkEndpoint\x1a3\n" + "\x05Scope\x12\x16\n" + "\x06domain\x18\x01 \x01(\tR\x06domain\x12\x12\n" + "\x04path\x18\x02 \x01(\tR\x04pathJ\x04\b\x04\x10\x05\"l\n" + "\vCrawlTarget\x12\x10\n" + "\x03url\x18\x01 \x01(\tR\x03url\x12\x1f\n" + "\vhttp_method\x18\x02 \x01(\tR\n" + "httpMethod\x12*\n" + "\x11http_request_body\x18\x03 \x01(\fR\x0fhttpRequestBody\"4\n" + "\n" + "HttpHeader\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x05value\x18\x02 \x01(\tR\x05value\"\xe4\x02\n" + "\vCrawlResult\x12=\n" + "\fcrawl_target\x18\x01 \x01(\v2\x1a.tsunami.proto.CrawlTargetR\vcrawlTarget\x12\x1f\n" + "\vcrawl_depth\x18\x02 \x01(\x05R\n" + "crawlDepth\x12#\n" + "\rresponse_code\x18\x03 \x01(\x05R\fresponseCode\x12!\n" + "\fcontent_type\x18\x04 \x01(\tR\vcontentType\x12\x18\n" + "\acontent\x18\x05 \x01(\fR\acontent\x12D\n" + "\x10response_headers\x18\x06 \x03(\v2\x19.tsunami.proto.HttpHeaderR\x0fresponseHeaders\x12M\n" + "\x12crawl_content_type\x18\a \x01(\x0e2\x1f.tsunami.proto.CrawlContentTypeR\x10crawlContentType*]\n" + "\x10CrawlContentType\x12\x1c\n" + "\x18CONTENT_TYPE_UNSPECIFIED\x10\x00\x12\x14\n" + "\x10CONTENT_TYPE_RAW\x10\x01\x12\x15\n" + "\x11CONTENT_TYPE_HASH\x10\x02Bt\n" + "\x18com.google.tsunami.protoB\x0eWebCrawlProtosP\x01ZFgithub.com/google/tsunami-security-scanner/proto/go/web_crawl_go_protob\x06proto3" var file_web_crawl_proto_enumTypes = make([]protoimpl.EnumInfo, 1) var file_web_crawl_proto_msgTypes = make([]protoimpl.MessageInfo, 5) var file_web_crawl_proto_goTypes = []any{ (CrawlContentType)(0), // 0: tsunami.proto.CrawlContentType (*CrawlConfig)(nil), // 1: tsunami.proto.CrawlConfig (*CrawlTarget)(nil), // 2: tsunami.proto.CrawlTarget (*HttpHeader)(nil), // 3: tsunami.proto.HttpHeader (*CrawlResult)(nil), // 4: tsunami.proto.CrawlResult (*CrawlConfig_Scope)(nil), // 5: tsunami.proto.CrawlConfig.Scope (*network_go_proto.NetworkEndpoint)(nil), // 6: tsunami.proto.NetworkEndpoint } var file_web_crawl_proto_depIdxs = []int32{ 5, // 0: tsunami.proto.CrawlConfig.scopes:type_name -> tsunami.proto.CrawlConfig.Scope 6, // 1: tsunami.proto.CrawlConfig.network_endpoint:type_name -> tsunami.proto.NetworkEndpoint 2, // 2: tsunami.proto.CrawlResult.crawl_target:type_name -> tsunami.proto.CrawlTarget 3, // 3: tsunami.proto.CrawlResult.response_headers:type_name -> tsunami.proto.HttpHeader 0, // 4: tsunami.proto.CrawlResult.crawl_content_type:type_name -> tsunami.proto.CrawlContentType 5, // [5:5] is the sub-list for method output_type 5, // [5:5] is the sub-list for method input_type 5, // [5:5] is the sub-list for extension type_name 5, // [5:5] is the sub-list for extension extendee 0, // [0:5] is the sub-list for field type_name } func init() { file_web_crawl_proto_init() } func file_web_crawl_proto_init() { if File_web_crawl_proto != nil { return } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_web_crawl_proto_rawDesc), len(file_web_crawl_proto_rawDesc)), NumEnums: 1, NumMessages: 5, NumExtensions: 0, NumServices: 0, }, GoTypes: file_web_crawl_proto_goTypes, DependencyIndexes: file_web_crawl_proto_depIdxs, EnumInfos: file_web_crawl_proto_enumTypes, MessageInfos: file_web_crawl_proto_msgTypes, }.Build() File_web_crawl_proto = out.File file_web_crawl_proto_goTypes = nil file_web_crawl_proto_depIdxs = nil } ================================================ FILE: proto/network.proto ================================================ /* * Copyright 2019 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ // Data models for describing network related information. syntax = "proto3"; package tsunami.proto; option java_multiple_files = true; option java_outer_classname = "NetworkProtos"; option java_package = "com.google.tsunami.proto"; option go_package = "github.com/google/tsunami-security-scanner/proto/go/network_go_proto"; // The address family of an IP address. enum AddressFamily { ADDRESS_FAMILY_UNSPECIFIED = 0; IPV4 = 4; IPV6 = 6; } // The IP address of a networking device. message IpAddress { // The family of the IP address. AddressFamily address_family = 1; // A human-readable representation of the IP address, e.g. 127.0.0.1 for IPV4 // and 2001:db8:0:1234:0:567:8:1 for IPV6. string address = 2; } // The port that a network service listens to. message Port { uint32 port_number = 1; } // The hostname of a networking device. message Hostname { string name = 1; } // A classification of an endpoint for a network device. message NetworkEndpoint { enum Type { TYPE_UNSPECIFIED = 0; // The network endpoint is represented by an IP address. IP = 1; // The network endpoint is represented by IP address and port pair. IP_PORT = 2; // The network endpoint is represented by a hostname. HOSTNAME = 3; // The network endpoint is represented by a hostname and port pair. HOSTNAME_PORT = 4; // The network endpoint is represented by an IP address and hostname. IP_HOSTNAME = 5; // The network endpoint is represented by an IP address, hostname and port. IP_HOSTNAME_PORT = 6; } // Type of the network endpoint. Type type = 1; // Optional IP address of a network endpoint. Must be specified when Type is // IP or IP_PORT. IpAddress ip_address = 2; // Optional port of a network endpoint. Must be specified when Type is IP_PORT // or HOSTNAME_PORT. Port port = 3; // Optional hostname of a network endpoint. Must be specified when Type is // HOSTNAME or HOSTNAME_PORT. Hostname hostname = 4; } // The transport layer protocols. enum TransportProtocol { TRANSPORT_PROTOCOL_UNSPECIFIED = 0; TCP = 1; UDP = 2; SCTP = 3; } ================================================ FILE: proto/network_service.proto ================================================ /* * Copyright 2019 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ // Data models for describing a network service. syntax = "proto3"; package tsunami.proto; import "network.proto"; import "software.proto"; import "web_crawl.proto"; option java_multiple_files = true; option java_outer_classname = "NetworkServiceProtos"; option java_package = "com.google.tsunami.proto"; option go_package = "github.com/google/tsunami-security-scanner/proto/go/network_service_go_proto"; // General information about a network service running on a target. message NetworkService { // The network endpoint where this network service is served. NetworkEndpoint network_endpoint = 1; // The transport layer protocol used by the service. TransportProtocol transport_protocol = 2; // The name of the network service, following convention in RFC6335. Examples // are like http, telnet, ssh, etc. string service_name = 3; // The software that provides the service behind the port. Software software = 4; // The complete set of versions of the software. VersionSet version_set = 5; // Banners generated by the service. repeated string banner = 6; // Context information about this network service. ServiceContext service_context = 7; // The detected Common Platform Enumeration (CPE) name for service, // in the uri binding representation, like: cpe:/a:openbsd:openssh:8.4p1 repeated string cpes = 8; // List of supported SSL versions (e.g. TLSv1, SSLv3, ...) on the service. repeated string supported_ssl_versions = 9; // List of supported HTTP methods (e.g. POST, GET, ...) on the service. repeated string supported_http_methods = 10; } // Context information about a specific network service. message ServiceContext { oneof context { WebServiceContext web_service_context = 1; } } // Context information about a web application. // NEXT ID: 5 message WebServiceContext { // The root path of the hosted web application. string application_root = 1; // The web application that is serving under the application root. Software software = 2; // The detected versions of the web application. VersionSet version_set = 3; // Fingerprinter's crawling results for this web service. repeated CrawlResult crawl_results = 4; } ================================================ FILE: proto/payload_generator.proto ================================================ /* * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ // Data models utilized by the Tsunami Paylaod Generator syntax = "proto3"; package tsunami.proto; import 'google/protobuf/wrappers.proto'; option java_multiple_files = true; option java_outer_classname = "PayloadGeneratorProtos"; option java_package = "com.google.tsunami.proto"; option go_package = "github.com/google/tsunami-security-scanner/proto/go/payload_generator_go_proto"; // Attributes utilized by the PayloadGenerator to select a payload message PayloadGeneratorConfig { // The type of vulnerability the detector is looking for enum VulnerabilityType { // Unspecified vulnerability type VULNERABILITY_TYPE_UNSPECIFIED = 0; // RCE which returns the output of the execution REFLECTIVE_RCE = 1; // RCE which does not return the output of the execution BLIND_RCE = 2; // Server-Side Request Forgery SSRF = 3; // Arbitrary File Write ARBITRARY_FILE_WRITE = 4; // RCE without output of the execution + File Read (needed to get // confirmation string) BLIND_RCE_FILE_READ = 5; } // The environment that processes the payload for execution e.g. a PHP-based // target likely wants a payload that is itself PHP code. enum InterpretationEnvironment { // Unspecified interpretation environment type INTERPRETATION_ENVIRONMENT_UNSPECIFIED = 0; // Payload is interpreted within a Linux shell environment LINUX_SHELL = 1; // Payload is interpreted wihin a Java compiler context JAVA = 2; // Payload is interpreted wihin a PHP VM context PHP = 3; // Interpretation environment doesn't matter INTERPRETATION_ANY = 4; // Payload is interpreted wihin crontab LINUX_ROOT_CRONTAB = 5; // Payload is interpreted wihin a Windows shell environment WINDOWS_SHELL = 6; // Payload is interpreted within a JSP shell environment JSP = 7; } // The actual runtime environment when the payload is run e.g. while a // PHP-based target wants a PHP-interpretation environment, the actual code // execution may happen via the Linux shell: exec(“echo \”this is running in // the system.\””). enum ExecutionEnvironment { // Unspecified execution environment type EXECUTION_ENVIRONMENT_UNSPECIFIED = 0; // Execute within the InterpretationEnvironment EXEC_INTERPRETATION_ENVIRONMENT = 1; // Execution environment doesn't matter EXEC_ANY = 2; } VulnerabilityType vulnerability_type = 2; InterpretationEnvironment interpretation_environment = 3; ExecutionEnvironment execution_environment = 4; } // Attributes of a payload. A detector can check these attributes to change its // logic based on the payload type. message PayloadAttributes { // Whether the payload uses the callback server bool uses_callback_server = 1; } enum PayloadValidationType { VALIDATION_TYPE_UNSPECIFIED = 0; VALIDATION_REGEX = 1; } // Container type for payload_definitions.yaml message PayloadLibrary { repeated PayloadDefinition payloads = 1; } // Schema for each entry in payload_definitions.yaml // Note: this message uses StringValue and BoolValue because we validate whether // each payload definition in the yaml file has the correct fields present. // Since empty proto fields are given default values (proto fields are not // nullable), we use the wrapped types to check for actual presence. message PayloadDefinition { // The human-readable string to identify the payload google.protobuf.StringValue name = 1; PayloadGeneratorConfig.InterpretationEnvironment interpretation_environment = 2; PayloadGeneratorConfig.ExecutionEnvironment execution_environment = 3; // All vulnerability types this payload can be used for repeated PayloadGeneratorConfig.VulnerabilityType vulnerability_type = 4; // If true, payload_string must contain the $TSUNAMI_PAYLOAD_TOKEN_URL // token. Validation will automatically check against the callback server, so // the validation* fields do not need to be set. google.protobuf.BoolValue uses_callback_server = 5; // The actual payload command string. The following special tokens can be // used which will cause the framework to inject dynamic content into the // command: // - $TSUNAMI_PAYLOAD_TOKEN_URL: url for the callback server // - a random string, used to reduce false positives. google.protobuf.StringValue payload_string = 6; // The type of validation function for determining if the payload was // executed. Currently, only REGEX is supported. PayloadValidationType validation_type = 7; // Required if validation_type == REGEX. Must be compatible with // java.util.regex.Pattern. The string will first be preprocessed before // applied as a regex, replacing any of the following tokens with the // corresponding values supplied by the framework: // - $TSUNAMI_PAYLOAD_TOKEN_RANDOM: a random string, used to reduce false // positives. The value is guaranteed to be the same as the value supplied // to payload_string. google.protobuf.StringValue validation_regex = 8; } ================================================ FILE: proto/plugin_representation.proto ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ // Representation of a tsunami plugin definition passed between language // servers. syntax = "proto3"; package tsunami.proto; option java_multiple_files = true; option java_outer_classname = "PluginRepresentationProtos"; option java_package = "com.google.tsunami.proto"; option go_package = "github.com/google/tsunami-security-scanner/proto/go/plugin_representation_go_proto"; // Represents a PluginDefinition placeholder. message PluginDefinition { // PluginInfo of this definition. PluginInfo info = 1; // The name of the target service. TargetServiceName target_service_name = 2; // The name of the target software. TargetSoftware target_software = 3; // If the definition is for a web service or not. bool for_web_service = 4; // If the definition is for a specific operating system or not. // Note: this filter is executed within an AND condition with the other // filters. E.g. if target_service_name.value is "http" and // target_operating_system.osclass.family is "Linux" then the plugin will only // match if the service is http and the operating system is Linux. TargetOperatingSystemClass target_operating_system_class = 5; } // Represents a PluginInfo annotation placeholder used by the // PluginDefinition proto above. message PluginInfo { enum PluginType { // Plugin is an unspecified type. PLUGIN_TYPE_UNSPECIFIED = 0; // Plugin is a port scanner. PORT_SCAN = 1; // Plugin is a service fingerprinter. SERVICE_FINGERPRINT = 2; // Plugin is a vulnerability detector. VULN_DETECTION = 3; } // Type of plugin. PluginType type = 1; // Name of the plugin. string name = 2; // Version of the plugin string version = 3; // Description of the plugin. string description = 4; // Author of the plugin. string author = 5; } // Represents a ForServiceName annotation placeholder used by the // PluginDefinition proto above. message TargetServiceName { // The value of the name of the target. repeated string value = 1; } // Represents a ForSoftware annotation placeholder used by the // PluginDefinition proto above. message TargetSoftware { // The name of the target software, case insensitive. string name = 1; // Array of versions and version ranges of the target software. repeated string value = 2; } // Represents a ForOperatingSystem annotation placeholder used by the // PluginDefinition proto above. These values are coming directly from the // port scanner's output (e.g. nmap). message TargetOperatingSystemClass { // The vendor of the target operating system, e.g. "Microsoft" repeated string vendor = 1; // The family of the target operating system, e.g. "Windows" repeated string os_family = 2; // The minimum accuracy of the target operating system, e.g. 90 uint32 min_accuracy = 3; } ================================================ FILE: proto/plugin_service.proto ================================================ /* * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ // Model for the plugin RPC service protocol. syntax = "proto3"; package tsunami.proto; import "detection.proto"; import "network_service.proto"; import "plugin_representation.proto"; import "reconnaissance.proto"; option java_multiple_files = true; option java_outer_classname = "PluginServiceProtos"; option java_package = "com.google.tsunami.proto"; option go_package = "github.com/google/tsunami-security-scanner/proto/go/plugin_service_go_proto"; // Represents a run request with all matched plugins that will need to run // as well as the target to run against. message RunRequest { // Target of the plugins. TargetInfo target = 1; // All matched plugins that will need to run. repeated MatchedPlugin plugins = 2; } // Compact representation of RunRequest. message RunCompactRequest { // Target of the plugins. TargetInfo target = 1; // Indexes in the following structure point to the services/plugins defined // below. (The order is safe, guaranteed by the proto specification: "The // order of the elements with respect to each other is preserved when parsing, // though the ordering with respect to other fields is lost.") message PluginNetworkServiceTarget { // The index of the plugin to run. uint32 plugin_index = 1; // The index of the network service to run against. uint32 service_index = 2; } // All network services that are targeted by some of the plugins. repeated NetworkService services = 2; // All plugins that should be executed during the run. repeated PluginDefinition plugins = 3; // The concrete map of plugin/network service pairs that should be scanned. repeated PluginNetworkServiceTarget scan_targets = 4; } // Represents the plugin needed to run by the language-specific server // as well as all the matched network services for the plugin. message MatchedPlugin { // All matched network services from the reconnaissance report. repeated NetworkService services = 1; // Plugin to run. PluginDefinition plugin = 2; } // Represents a run response with the only field being all DetectionReports // generated by the language-specific server. message RunResponse { DetectionReportList reports = 1; } // Represents a request to list all plugins from the requested server. message ListPluginsRequest {} // Represents a response containing a list of all plugins // from the requested server. message ListPluginsResponse { repeated PluginDefinition plugins = 1; // Plugin service can indicate here that it RunRequest should be compact // (compact_targets should be populated instead of MatchedPlugin plugins). bool want_compact_run_request = 2; } // Represents the plugin service, two RPCs for running plugins // and listing plugins, respectively. service PluginService { // Performs a run request to run all language plugins specified by the // request. rpc Run(RunRequest) returns (RunResponse) {} // Performs a run request to run all language plugins specified by the // compact representation of the request. This is useful, when hundreds of // plugins are to be run against many different NetworkServices. // The language server must set `want_compact_run_request` so that the // Tsunami CLI knows to invoke this method instead of `Run`. rpc RunCompact(RunCompactRequest) returns (RunResponse) {} // Sends a request to list all plugins from the respective language server. rpc ListPlugins(ListPluginsRequest) returns (ListPluginsResponse) {} } ================================================ FILE: proto/reconnaissance.proto ================================================ /* * Copyright 2019 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ // Data models for all the reconnaissance information gathered by Tsunami. syntax = "proto3"; package tsunami.proto; import "network.proto"; import "network_service.proto"; option java_multiple_files = true; option java_outer_classname = "ReconnaissanceProtos"; option java_package = "com.google.tsunami.proto"; option go_package = "github.com/google/tsunami-security-scanner/proto/go/reconnaissance_go_proto"; // Detailed information about the scanning target. message TargetInfo { // All the known network endpoints of the scanning target. repeated NetworkEndpoint network_endpoints = 1; // TODO(magl): add more information about the scanning target, like OSes, // architectures, firewalls etc. repeated OperatingSystemClass operating_system_classes = 2; } // Represents a ForOperatingSystem annotation placeholder used by the // PluginDefinition proto above. // For possible values, consult the following database: // https://raw.githubusercontent.com/nmap/nmap/master/nmap-os-db message OperatingSystemClass { // The type of the target operating system, e.g. "general purpose" string type = 1; // The vendor of the target operating system, e.g. "Linux" string vendor = 2; // The family of the target operating system, e.g. "Linux" string os_family = 3; // The generation of the target operating system, e.g. "2.6.X" string os_generation = 4; // The estimated accuracy of the target operating system, e.g. 90 uint32 accuracy = 5; } // Report from a port scanner. message PortScanningReport { // Information about the scanning target. TargetInfo target_info = 1; // List of all the exposed network services. repeated NetworkService network_services = 2; } // Report from a service fingerprinter. message FingerprintingReport { // List of all the identified network services after fingerprinting. repeated NetworkService network_services = 3; } // Full reconnaissance report about a single scanning target. message ReconnaissanceReport { // Information about the scanning target. TargetInfo target_info = 1; // All exposed network services of the scanning target. repeated NetworkService network_services = 2; } ================================================ FILE: proto/scan_results.proto ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ // Data models for describing scanning results. syntax = "proto3"; package tsunami.proto; import "google/protobuf/timestamp.proto"; import "google/protobuf/duration.proto"; import "detection.proto"; import "network_service.proto"; import "reconnaissance.proto"; import "vulnerability.proto"; option java_multiple_files = true; option java_outer_classname = "ScanResultsProtos"; option java_package = "com.google.tsunami.proto"; option go_package = "github.com/google/tsunami-security-scanner/proto/go/scan_results_go_proto"; // Execution status of the scan. // NEXT ID: 5 enum ScanStatus { // Unspecified status. SCAN_STATUS_UNSPECIFIED = 0; // Scan finished successfully. SUCCEEDED = 1; // Scan finished with only a small set of selected detectors succeeded. PARTIALLY_SUCCEEDED = 4; // Scan failed. FAILED = 2; // Scan cancelled. CANCELLED = 3; } // A single vulnerability finding for a specific service. message ScanFinding { // Information about the scanned target. TargetInfo target_info = 1; // Information about the scanned network service. NetworkService network_service = 2; // Details about the detected vulnerability. Vulnerability vulnerability = 3; } // Full scanning results. // NEXT ID: 9 message ScanResults { // Status of this scan. ScanStatus scan_status = 1; // Detailed message for the scan status. string status_message = 6; // Reports whether the target was alive during the scan. // A target is considered alive if at least one network service was identified // or at least one vulnerability was detected. bool target_alive = 8; // All findings from this scan. repeated ScanFinding scan_findings = 2; // Time when this scan was started. google.protobuf.Timestamp scan_start_timestamp = 3; // Duration of the full scan. google.protobuf.Duration scan_duration = 4; // Detection reports from all triggered Tsunami detection plugins. FullDetectionReports full_detection_reports = 5; // Reconnaissance reports from the fingerprinting stage. ReconnaissanceReport reconnaissance_report = 7; } // Full detection reports from all triggered Tsunami detection plugins. message FullDetectionReports { repeated DetectionReport detection_reports = 1; } ================================================ FILE: proto/scan_target.proto ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ // Data models for describing a scanning target. syntax = "proto3"; package tsunami.proto; import "network.proto"; import "network_service.proto"; option java_multiple_files = true; option java_outer_classname = "ScanTargetProtos"; option java_package = "com.google.tsunami.proto"; option go_package = "github.com/google/tsunami-security-scanner/proto/go/scan_target_go_proto"; // The information about a scan target. message ScanTarget { oneof target { // The network endpoint to be scanned. NetworkEndpoint network_endpoint = 1; // The network service to be scanned. NetworkService network_service = 2; } } ================================================ FILE: proto/software.proto ================================================ /* * Copyright 2019 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ // Data models for describing a software. syntax = "proto3"; package tsunami.proto; option java_multiple_files = true; option java_outer_classname = "SoftwareProtos"; option java_package = "com.google.tsunami.proto"; option go_package = "github.com/google/tsunami-security-scanner/proto/go/software_go_proto"; // The exact version of a software. message Version { // Type of the Version message, identifying an ordinary software version or a // sentinel MINIMUM/MAXIMUM version. See comments below for what is a sentinel // version. enum VersionType { VERSION_TYPE_UNSPECIFIED = 0; // A normal software version. NORMAL = 1; // A sentinel version representing negative infinity, i.e. MINIMUM version // is less than any NORMAL and MAXIMUM versions. MINIMUM = 2; // A sentinel version representing positive infinity, i.e. MAXIMUM version // is greater than any NORMAL and MINIMUM versions. MAXIMUM = 3; } // Distinguishes between sentinel MIN/MAX versions and normal versions. VersionType type = 1; // Human readable version number, e.g. 1.0.3. This is set only when type is // NORMAL. Tsunami uses raw string to represent a version number instead of // any structured messages in order to handle different kinds of version // schemes. Tsunami will tokenize this version string and store tokens // internally. When performing version comparisons, Tsunami follows the // precedence defined by Semantic Versioning (semver.org). More details can be // found in Tsunami's internal Version class. string full_version_string = 2; } // An inclusive range of versions for a software. message VersionRange { // Whether the range endpoint is inclusive or exclusive. enum Inclusiveness { INCLUSIVENESS_UNSPECIFIED = 0; INCLUSIVE = 1; EXCLUSIVE = 2; } // Minimum version that belongs in the range. Version min_version = 1; // Inclusiveness of the min_version. When min_version points to negative // infinity, this value will always be EXCLUSIVE to matching the // representation of (-inf, 1.0]. Note that negative infinity version should // ***NOT*** be compared with a version range as it is just a bogus sentinel // version without any meaning. Inclusiveness min_version_inclusiveness = 2; // Maximum version that belongs in the range. Version max_version = 3; // Inclusiveness of the max_version. When max_version points to positive // infinity, this value will always be EXCLUSIVE to matching the // representation of [1.0, inf). Note that positive infinity version should // ***NOT*** be compared with a version range as it is just a bogus sentinel // version without any meaning. Inclusiveness max_version_inclusiveness = 4; } // A set of Versions and VersionRanges that completely describes a set of // software releases, e.g. {3.9.1, 3.9.3, [4.7.1, 4.7.8], 4.8} message VersionSet { repeated Version versions = 1; repeated VersionRange version_ranges = 2; } // A structured description about a software. message Software { // The name of this software. string name = 1; } ================================================ FILE: proto/tsunami_go_proto/detection.pb.go ================================================ // // Copyright 2020 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Data models for describing a vulnerability detection report. // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.5 // protoc v3.21.12 // source: detection.proto package tsunami_go_proto import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" timestamppb "google.golang.org/protobuf/types/known/timestamppb" reflect "reflect" sync "sync" unsafe "unsafe" ) const ( // Verify that this generated code is sufficiently up-to-date. _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) // Verify that runtime/protoimpl is sufficiently up-to-date. _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) // Status of the vulnerability detection result. type DetectionStatus int32 const ( // Unspecified status. DetectionStatus_DETECTION_STATUS_UNSPECIFIED DetectionStatus = 0 // Target is not vulnerable. DetectionStatus_SAFE DetectionStatus = 1 // Target appears to be vulnerable (e.g. because running version is // vulnerable), but couldn't be verified. DetectionStatus_VULNERABILITY_PRESENT DetectionStatus = 2 // Target is vulnerable and the detector successfully verified the // vulnerability. DetectionStatus_VULNERABILITY_VERIFIED DetectionStatus = 3 ) // Enum value maps for DetectionStatus. var ( DetectionStatus_name = map[int32]string{ 0: "DETECTION_STATUS_UNSPECIFIED", 1: "SAFE", 2: "VULNERABILITY_PRESENT", 3: "VULNERABILITY_VERIFIED", } DetectionStatus_value = map[string]int32{ "DETECTION_STATUS_UNSPECIFIED": 0, "SAFE": 1, "VULNERABILITY_PRESENT": 2, "VULNERABILITY_VERIFIED": 3, } ) func (x DetectionStatus) Enum() *DetectionStatus { p := new(DetectionStatus) *p = x return p } func (x DetectionStatus) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (DetectionStatus) Descriptor() protoreflect.EnumDescriptor { return file_detection_proto_enumTypes[0].Descriptor() } func (DetectionStatus) Type() protoreflect.EnumType { return &file_detection_proto_enumTypes[0] } func (x DetectionStatus) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } // Deprecated: Use DetectionStatus.Descriptor instead. func (DetectionStatus) EnumDescriptor() ([]byte, []int) { return file_detection_proto_rawDescGZIP(), []int{0} } // Full report about a detected vulnerability. type DetectionReport struct { state protoimpl.MessageState `protogen:"open.v1"` // Information about the scanned target. TargetInfo *TargetInfo `protobuf:"bytes,1,opt,name=target_info,json=targetInfo,proto3" json:"target_info,omitempty"` // Information about the scanned network service. NetworkService *NetworkService `protobuf:"bytes,2,opt,name=network_service,json=networkService,proto3" json:"network_service,omitempty"` // Time when the vulnerability was detected. DetectionTimestamp *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=detection_timestamp,json=detectionTimestamp,proto3" json:"detection_timestamp,omitempty"` // Status of the detection result. DetectionStatus DetectionStatus `protobuf:"varint,4,opt,name=detection_status,json=detectionStatus,proto3,enum=tsunami.proto.DetectionStatus" json:"detection_status,omitempty"` // Full details about the detected vulnerability. Vulnerability *Vulnerability `protobuf:"bytes,5,opt,name=vulnerability,proto3" json:"vulnerability,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *DetectionReport) Reset() { *x = DetectionReport{} mi := &file_detection_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *DetectionReport) String() string { return protoimpl.X.MessageStringOf(x) } func (*DetectionReport) ProtoMessage() {} func (x *DetectionReport) ProtoReflect() protoreflect.Message { mi := &file_detection_proto_msgTypes[0] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use DetectionReport.ProtoReflect.Descriptor instead. func (*DetectionReport) Descriptor() ([]byte, []int) { return file_detection_proto_rawDescGZIP(), []int{0} } func (x *DetectionReport) GetTargetInfo() *TargetInfo { if x != nil { return x.TargetInfo } return nil } func (x *DetectionReport) GetNetworkService() *NetworkService { if x != nil { return x.NetworkService } return nil } func (x *DetectionReport) GetDetectionTimestamp() *timestamppb.Timestamp { if x != nil { return x.DetectionTimestamp } return nil } func (x *DetectionReport) GetDetectionStatus() DetectionStatus { if x != nil { return x.DetectionStatus } return DetectionStatus_DETECTION_STATUS_UNSPECIFIED } func (x *DetectionReport) GetVulnerability() *Vulnerability { if x != nil { return x.Vulnerability } return nil } type DetectionReportList struct { state protoimpl.MessageState `protogen:"open.v1"` DetectionReports []*DetectionReport `protobuf:"bytes,1,rep,name=detection_reports,json=detectionReports,proto3" json:"detection_reports,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *DetectionReportList) Reset() { *x = DetectionReportList{} mi := &file_detection_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *DetectionReportList) String() string { return protoimpl.X.MessageStringOf(x) } func (*DetectionReportList) ProtoMessage() {} func (x *DetectionReportList) ProtoReflect() protoreflect.Message { mi := &file_detection_proto_msgTypes[1] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use DetectionReportList.ProtoReflect.Descriptor instead. func (*DetectionReportList) Descriptor() ([]byte, []int) { return file_detection_proto_rawDescGZIP(), []int{1} } func (x *DetectionReportList) GetDetectionReports() []*DetectionReport { if x != nil { return x.DetectionReports } return nil } var File_detection_proto protoreflect.FileDescriptor var file_detection_proto_rawDesc = string([]byte{ 0x0a, 0x0f, 0x64, 0x65, 0x74, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0d, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x15, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x14, 0x72, 0x65, 0x63, 0x6f, 0x6e, 0x6e, 0x61, 0x69, 0x73, 0x73, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x13, 0x76, 0x75, 0x6c, 0x6e, 0x65, 0x72, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xf1, 0x02, 0x0a, 0x0f, 0x44, 0x65, 0x74, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x3a, 0x0a, 0x0b, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0a, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x46, 0x0a, 0x0f, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x0e, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x4b, 0x0a, 0x13, 0x64, 0x65, 0x74, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x12, 0x64, 0x65, 0x74, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x49, 0x0a, 0x10, 0x64, 0x65, 0x74, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1e, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x44, 0x65, 0x74, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x0f, 0x64, 0x65, 0x74, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x42, 0x0a, 0x0d, 0x76, 0x75, 0x6c, 0x6e, 0x65, 0x72, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x56, 0x75, 0x6c, 0x6e, 0x65, 0x72, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x52, 0x0d, 0x76, 0x75, 0x6c, 0x6e, 0x65, 0x72, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x22, 0x62, 0x0a, 0x13, 0x44, 0x65, 0x74, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x4b, 0x0a, 0x11, 0x64, 0x65, 0x74, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x44, 0x65, 0x74, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x10, 0x64, 0x65, 0x74, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x73, 0x2a, 0x74, 0x0a, 0x0f, 0x44, 0x65, 0x74, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x20, 0x0a, 0x1c, 0x44, 0x45, 0x54, 0x45, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x53, 0x41, 0x46, 0x45, 0x10, 0x01, 0x12, 0x19, 0x0a, 0x15, 0x56, 0x55, 0x4c, 0x4e, 0x45, 0x52, 0x41, 0x42, 0x49, 0x4c, 0x49, 0x54, 0x59, 0x5f, 0x50, 0x52, 0x45, 0x53, 0x45, 0x4e, 0x54, 0x10, 0x02, 0x12, 0x1a, 0x0a, 0x16, 0x56, 0x55, 0x4c, 0x4e, 0x45, 0x52, 0x41, 0x42, 0x49, 0x4c, 0x49, 0x54, 0x59, 0x5f, 0x56, 0x45, 0x52, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x03, 0x42, 0x70, 0x0a, 0x18, 0x63, 0x6f, 0x6d, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x42, 0x0f, 0x44, 0x65, 0x74, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x50, 0x01, 0x5a, 0x41, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2d, 0x73, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x2d, 0x73, 0x63, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x5f, 0x67, 0x6f, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, }) var ( file_detection_proto_rawDescOnce sync.Once file_detection_proto_rawDescData []byte ) func file_detection_proto_rawDescGZIP() []byte { file_detection_proto_rawDescOnce.Do(func() { file_detection_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_detection_proto_rawDesc), len(file_detection_proto_rawDesc))) }) return file_detection_proto_rawDescData } var file_detection_proto_enumTypes = make([]protoimpl.EnumInfo, 1) var file_detection_proto_msgTypes = make([]protoimpl.MessageInfo, 2) var file_detection_proto_goTypes = []any{ (DetectionStatus)(0), // 0: tsunami.proto.DetectionStatus (*DetectionReport)(nil), // 1: tsunami.proto.DetectionReport (*DetectionReportList)(nil), // 2: tsunami.proto.DetectionReportList (*TargetInfo)(nil), // 3: tsunami.proto.TargetInfo (*NetworkService)(nil), // 4: tsunami.proto.NetworkService (*timestamppb.Timestamp)(nil), // 5: google.protobuf.Timestamp (*Vulnerability)(nil), // 6: tsunami.proto.Vulnerability } var file_detection_proto_depIdxs = []int32{ 3, // 0: tsunami.proto.DetectionReport.target_info:type_name -> tsunami.proto.TargetInfo 4, // 1: tsunami.proto.DetectionReport.network_service:type_name -> tsunami.proto.NetworkService 5, // 2: tsunami.proto.DetectionReport.detection_timestamp:type_name -> google.protobuf.Timestamp 0, // 3: tsunami.proto.DetectionReport.detection_status:type_name -> tsunami.proto.DetectionStatus 6, // 4: tsunami.proto.DetectionReport.vulnerability:type_name -> tsunami.proto.Vulnerability 1, // 5: tsunami.proto.DetectionReportList.detection_reports:type_name -> tsunami.proto.DetectionReport 6, // [6:6] is the sub-list for method output_type 6, // [6:6] is the sub-list for method input_type 6, // [6:6] is the sub-list for extension type_name 6, // [6:6] is the sub-list for extension extendee 0, // [0:6] is the sub-list for field type_name } func init() { file_detection_proto_init() } func file_detection_proto_init() { if File_detection_proto != nil { return } file_network_service_proto_init() file_reconnaissance_proto_init() file_vulnerability_proto_init() type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_detection_proto_rawDesc), len(file_detection_proto_rawDesc)), NumEnums: 1, NumMessages: 2, NumExtensions: 0, NumServices: 0, }, GoTypes: file_detection_proto_goTypes, DependencyIndexes: file_detection_proto_depIdxs, EnumInfos: file_detection_proto_enumTypes, MessageInfos: file_detection_proto_msgTypes, }.Build() File_detection_proto = out.File file_detection_proto_goTypes = nil file_detection_proto_depIdxs = nil } ================================================ FILE: proto/tsunami_go_proto/network.pb.go ================================================ // // Copyright 2019 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Data models for describing network related information. // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.5 // protoc v3.21.12 // source: network.proto package tsunami_go_proto import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" unsafe "unsafe" ) const ( // Verify that this generated code is sufficiently up-to-date. _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) // Verify that runtime/protoimpl is sufficiently up-to-date. _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) // The address family of an IP address. type AddressFamily int32 const ( AddressFamily_ADDRESS_FAMILY_UNSPECIFIED AddressFamily = 0 AddressFamily_IPV4 AddressFamily = 4 AddressFamily_IPV6 AddressFamily = 6 ) // Enum value maps for AddressFamily. var ( AddressFamily_name = map[int32]string{ 0: "ADDRESS_FAMILY_UNSPECIFIED", 4: "IPV4", 6: "IPV6", } AddressFamily_value = map[string]int32{ "ADDRESS_FAMILY_UNSPECIFIED": 0, "IPV4": 4, "IPV6": 6, } ) func (x AddressFamily) Enum() *AddressFamily { p := new(AddressFamily) *p = x return p } func (x AddressFamily) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (AddressFamily) Descriptor() protoreflect.EnumDescriptor { return file_network_proto_enumTypes[0].Descriptor() } func (AddressFamily) Type() protoreflect.EnumType { return &file_network_proto_enumTypes[0] } func (x AddressFamily) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } // Deprecated: Use AddressFamily.Descriptor instead. func (AddressFamily) EnumDescriptor() ([]byte, []int) { return file_network_proto_rawDescGZIP(), []int{0} } // The transport layer protocols. type TransportProtocol int32 const ( TransportProtocol_TRANSPORT_PROTOCOL_UNSPECIFIED TransportProtocol = 0 TransportProtocol_TCP TransportProtocol = 1 TransportProtocol_UDP TransportProtocol = 2 TransportProtocol_SCTP TransportProtocol = 3 ) // Enum value maps for TransportProtocol. var ( TransportProtocol_name = map[int32]string{ 0: "TRANSPORT_PROTOCOL_UNSPECIFIED", 1: "TCP", 2: "UDP", 3: "SCTP", } TransportProtocol_value = map[string]int32{ "TRANSPORT_PROTOCOL_UNSPECIFIED": 0, "TCP": 1, "UDP": 2, "SCTP": 3, } ) func (x TransportProtocol) Enum() *TransportProtocol { p := new(TransportProtocol) *p = x return p } func (x TransportProtocol) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (TransportProtocol) Descriptor() protoreflect.EnumDescriptor { return file_network_proto_enumTypes[1].Descriptor() } func (TransportProtocol) Type() protoreflect.EnumType { return &file_network_proto_enumTypes[1] } func (x TransportProtocol) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } // Deprecated: Use TransportProtocol.Descriptor instead. func (TransportProtocol) EnumDescriptor() ([]byte, []int) { return file_network_proto_rawDescGZIP(), []int{1} } type NetworkEndpoint_Type int32 const ( NetworkEndpoint_TYPE_UNSPECIFIED NetworkEndpoint_Type = 0 // The network endpoint is represented by an IP address. NetworkEndpoint_IP NetworkEndpoint_Type = 1 // The network endpoint is represented by IP address and port pair. NetworkEndpoint_IP_PORT NetworkEndpoint_Type = 2 // The network endpoint is represented by a hostname. NetworkEndpoint_HOSTNAME NetworkEndpoint_Type = 3 // The network endpoint is represented by a hostname and port pair. NetworkEndpoint_HOSTNAME_PORT NetworkEndpoint_Type = 4 // The network endpoint is represented by an IP address and hostname. NetworkEndpoint_IP_HOSTNAME NetworkEndpoint_Type = 5 // The network endpoint is represented by an IP address, hostname and port. NetworkEndpoint_IP_HOSTNAME_PORT NetworkEndpoint_Type = 6 ) // Enum value maps for NetworkEndpoint_Type. var ( NetworkEndpoint_Type_name = map[int32]string{ 0: "TYPE_UNSPECIFIED", 1: "IP", 2: "IP_PORT", 3: "HOSTNAME", 4: "HOSTNAME_PORT", 5: "IP_HOSTNAME", 6: "IP_HOSTNAME_PORT", } NetworkEndpoint_Type_value = map[string]int32{ "TYPE_UNSPECIFIED": 0, "IP": 1, "IP_PORT": 2, "HOSTNAME": 3, "HOSTNAME_PORT": 4, "IP_HOSTNAME": 5, "IP_HOSTNAME_PORT": 6, } ) func (x NetworkEndpoint_Type) Enum() *NetworkEndpoint_Type { p := new(NetworkEndpoint_Type) *p = x return p } func (x NetworkEndpoint_Type) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (NetworkEndpoint_Type) Descriptor() protoreflect.EnumDescriptor { return file_network_proto_enumTypes[2].Descriptor() } func (NetworkEndpoint_Type) Type() protoreflect.EnumType { return &file_network_proto_enumTypes[2] } func (x NetworkEndpoint_Type) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } // Deprecated: Use NetworkEndpoint_Type.Descriptor instead. func (NetworkEndpoint_Type) EnumDescriptor() ([]byte, []int) { return file_network_proto_rawDescGZIP(), []int{3, 0} } // The IP address of a networking device. type IpAddress struct { state protoimpl.MessageState `protogen:"open.v1"` // The family of the IP address. AddressFamily AddressFamily `protobuf:"varint,1,opt,name=address_family,json=addressFamily,proto3,enum=tsunami.proto.AddressFamily" json:"address_family,omitempty"` // A human-readable representation of the IP address, e.g. 127.0.0.1 for IPV4 // and 2001:db8:0:1234:0:567:8:1 for IPV6. Address string `protobuf:"bytes,2,opt,name=address,proto3" json:"address,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *IpAddress) Reset() { *x = IpAddress{} mi := &file_network_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *IpAddress) String() string { return protoimpl.X.MessageStringOf(x) } func (*IpAddress) ProtoMessage() {} func (x *IpAddress) ProtoReflect() protoreflect.Message { mi := &file_network_proto_msgTypes[0] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use IpAddress.ProtoReflect.Descriptor instead. func (*IpAddress) Descriptor() ([]byte, []int) { return file_network_proto_rawDescGZIP(), []int{0} } func (x *IpAddress) GetAddressFamily() AddressFamily { if x != nil { return x.AddressFamily } return AddressFamily_ADDRESS_FAMILY_UNSPECIFIED } func (x *IpAddress) GetAddress() string { if x != nil { return x.Address } return "" } // The port that a network service listens to. type Port struct { state protoimpl.MessageState `protogen:"open.v1"` PortNumber uint32 `protobuf:"varint,1,opt,name=port_number,json=portNumber,proto3" json:"port_number,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *Port) Reset() { *x = Port{} mi := &file_network_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *Port) String() string { return protoimpl.X.MessageStringOf(x) } func (*Port) ProtoMessage() {} func (x *Port) ProtoReflect() protoreflect.Message { mi := &file_network_proto_msgTypes[1] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Port.ProtoReflect.Descriptor instead. func (*Port) Descriptor() ([]byte, []int) { return file_network_proto_rawDescGZIP(), []int{1} } func (x *Port) GetPortNumber() uint32 { if x != nil { return x.PortNumber } return 0 } // The hostname of a networking device. type Hostname struct { state protoimpl.MessageState `protogen:"open.v1"` Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *Hostname) Reset() { *x = Hostname{} mi := &file_network_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *Hostname) String() string { return protoimpl.X.MessageStringOf(x) } func (*Hostname) ProtoMessage() {} func (x *Hostname) ProtoReflect() protoreflect.Message { mi := &file_network_proto_msgTypes[2] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Hostname.ProtoReflect.Descriptor instead. func (*Hostname) Descriptor() ([]byte, []int) { return file_network_proto_rawDescGZIP(), []int{2} } func (x *Hostname) GetName() string { if x != nil { return x.Name } return "" } // A classification of an endpoint for a network device. type NetworkEndpoint struct { state protoimpl.MessageState `protogen:"open.v1"` // Type of the network endpoint. Type NetworkEndpoint_Type `protobuf:"varint,1,opt,name=type,proto3,enum=tsunami.proto.NetworkEndpoint_Type" json:"type,omitempty"` // Optional IP address of a network endpoint. Must be specified when Type is // IP or IP_PORT. IpAddress *IpAddress `protobuf:"bytes,2,opt,name=ip_address,json=ipAddress,proto3" json:"ip_address,omitempty"` // Optional port of a network endpoint. Must be specified when Type is IP_PORT // or HOSTNAME_PORT. Port *Port `protobuf:"bytes,3,opt,name=port,proto3" json:"port,omitempty"` // Optional hostname of a network endpoint. Must be specified when Type is // HOSTNAME or HOSTNAME_PORT. Hostname *Hostname `protobuf:"bytes,4,opt,name=hostname,proto3" json:"hostname,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *NetworkEndpoint) Reset() { *x = NetworkEndpoint{} mi := &file_network_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *NetworkEndpoint) String() string { return protoimpl.X.MessageStringOf(x) } func (*NetworkEndpoint) ProtoMessage() {} func (x *NetworkEndpoint) ProtoReflect() protoreflect.Message { mi := &file_network_proto_msgTypes[3] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use NetworkEndpoint.ProtoReflect.Descriptor instead. func (*NetworkEndpoint) Descriptor() ([]byte, []int) { return file_network_proto_rawDescGZIP(), []int{3} } func (x *NetworkEndpoint) GetType() NetworkEndpoint_Type { if x != nil { return x.Type } return NetworkEndpoint_TYPE_UNSPECIFIED } func (x *NetworkEndpoint) GetIpAddress() *IpAddress { if x != nil { return x.IpAddress } return nil } func (x *NetworkEndpoint) GetPort() *Port { if x != nil { return x.Port } return nil } func (x *NetworkEndpoint) GetHostname() *Hostname { if x != nil { return x.Hostname } return nil } var File_network_proto protoreflect.FileDescriptor var file_network_proto_rawDesc = string([]byte{ 0x0a, 0x0d, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0d, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x6a, 0x0a, 0x09, 0x49, 0x70, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x43, 0x0a, 0x0e, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x5f, 0x66, 0x61, 0x6d, 0x69, 0x6c, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1c, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x46, 0x61, 0x6d, 0x69, 0x6c, 0x79, 0x52, 0x0d, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x46, 0x61, 0x6d, 0x69, 0x6c, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x22, 0x27, 0x0a, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x70, 0x6f, 0x72, 0x74, 0x5f, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0a, 0x70, 0x6f, 0x72, 0x74, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x22, 0x1e, 0x0a, 0x08, 0x48, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0xdc, 0x02, 0x0a, 0x0f, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x37, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x23, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x2e, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x37, 0x0a, 0x0a, 0x69, 0x70, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x49, 0x70, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x52, 0x09, 0x69, 0x70, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x27, 0x0a, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x52, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x33, 0x0a, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x52, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x79, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x10, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x06, 0x0a, 0x02, 0x49, 0x50, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x49, 0x50, 0x5f, 0x50, 0x4f, 0x52, 0x54, 0x10, 0x02, 0x12, 0x0c, 0x0a, 0x08, 0x48, 0x4f, 0x53, 0x54, 0x4e, 0x41, 0x4d, 0x45, 0x10, 0x03, 0x12, 0x11, 0x0a, 0x0d, 0x48, 0x4f, 0x53, 0x54, 0x4e, 0x41, 0x4d, 0x45, 0x5f, 0x50, 0x4f, 0x52, 0x54, 0x10, 0x04, 0x12, 0x0f, 0x0a, 0x0b, 0x49, 0x50, 0x5f, 0x48, 0x4f, 0x53, 0x54, 0x4e, 0x41, 0x4d, 0x45, 0x10, 0x05, 0x12, 0x14, 0x0a, 0x10, 0x49, 0x50, 0x5f, 0x48, 0x4f, 0x53, 0x54, 0x4e, 0x41, 0x4d, 0x45, 0x5f, 0x50, 0x4f, 0x52, 0x54, 0x10, 0x06, 0x2a, 0x43, 0x0a, 0x0d, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x46, 0x61, 0x6d, 0x69, 0x6c, 0x79, 0x12, 0x1e, 0x0a, 0x1a, 0x41, 0x44, 0x44, 0x52, 0x45, 0x53, 0x53, 0x5f, 0x46, 0x41, 0x4d, 0x49, 0x4c, 0x59, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x50, 0x56, 0x34, 0x10, 0x04, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x50, 0x56, 0x36, 0x10, 0x06, 0x2a, 0x53, 0x0a, 0x11, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x22, 0x0a, 0x1e, 0x54, 0x52, 0x41, 0x4e, 0x53, 0x50, 0x4f, 0x52, 0x54, 0x5f, 0x50, 0x52, 0x4f, 0x54, 0x4f, 0x43, 0x4f, 0x4c, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x54, 0x43, 0x50, 0x10, 0x01, 0x12, 0x07, 0x0a, 0x03, 0x55, 0x44, 0x50, 0x10, 0x02, 0x12, 0x08, 0x0a, 0x04, 0x53, 0x43, 0x54, 0x50, 0x10, 0x03, 0x42, 0x6e, 0x0a, 0x18, 0x63, 0x6f, 0x6d, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x42, 0x0d, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x50, 0x01, 0x5a, 0x41, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2d, 0x73, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x2d, 0x73, 0x63, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x5f, 0x67, 0x6f, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, }) var ( file_network_proto_rawDescOnce sync.Once file_network_proto_rawDescData []byte ) func file_network_proto_rawDescGZIP() []byte { file_network_proto_rawDescOnce.Do(func() { file_network_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_network_proto_rawDesc), len(file_network_proto_rawDesc))) }) return file_network_proto_rawDescData } var file_network_proto_enumTypes = make([]protoimpl.EnumInfo, 3) var file_network_proto_msgTypes = make([]protoimpl.MessageInfo, 4) var file_network_proto_goTypes = []any{ (AddressFamily)(0), // 0: tsunami.proto.AddressFamily (TransportProtocol)(0), // 1: tsunami.proto.TransportProtocol (NetworkEndpoint_Type)(0), // 2: tsunami.proto.NetworkEndpoint.Type (*IpAddress)(nil), // 3: tsunami.proto.IpAddress (*Port)(nil), // 4: tsunami.proto.Port (*Hostname)(nil), // 5: tsunami.proto.Hostname (*NetworkEndpoint)(nil), // 6: tsunami.proto.NetworkEndpoint } var file_network_proto_depIdxs = []int32{ 0, // 0: tsunami.proto.IpAddress.address_family:type_name -> tsunami.proto.AddressFamily 2, // 1: tsunami.proto.NetworkEndpoint.type:type_name -> tsunami.proto.NetworkEndpoint.Type 3, // 2: tsunami.proto.NetworkEndpoint.ip_address:type_name -> tsunami.proto.IpAddress 4, // 3: tsunami.proto.NetworkEndpoint.port:type_name -> tsunami.proto.Port 5, // 4: tsunami.proto.NetworkEndpoint.hostname:type_name -> tsunami.proto.Hostname 5, // [5:5] is the sub-list for method output_type 5, // [5:5] is the sub-list for method input_type 5, // [5:5] is the sub-list for extension type_name 5, // [5:5] is the sub-list for extension extendee 0, // [0:5] is the sub-list for field type_name } func init() { file_network_proto_init() } func file_network_proto_init() { if File_network_proto != nil { return } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_network_proto_rawDesc), len(file_network_proto_rawDesc)), NumEnums: 3, NumMessages: 4, NumExtensions: 0, NumServices: 0, }, GoTypes: file_network_proto_goTypes, DependencyIndexes: file_network_proto_depIdxs, EnumInfos: file_network_proto_enumTypes, MessageInfos: file_network_proto_msgTypes, }.Build() File_network_proto = out.File file_network_proto_goTypes = nil file_network_proto_depIdxs = nil } ================================================ FILE: proto/tsunami_go_proto/network_service.pb.go ================================================ // // Copyright 2019 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Data models for describing a network service. // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.5 // protoc v3.21.12 // source: network_service.proto package tsunami_go_proto import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" unsafe "unsafe" ) const ( // Verify that this generated code is sufficiently up-to-date. _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) // Verify that runtime/protoimpl is sufficiently up-to-date. _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) // General information about a network service running on a target. type NetworkService struct { state protoimpl.MessageState `protogen:"open.v1"` // The network endpoint where this network service is served. NetworkEndpoint *NetworkEndpoint `protobuf:"bytes,1,opt,name=network_endpoint,json=networkEndpoint,proto3" json:"network_endpoint,omitempty"` // The transport layer protocol used by the service. TransportProtocol TransportProtocol `protobuf:"varint,2,opt,name=transport_protocol,json=transportProtocol,proto3,enum=tsunami.proto.TransportProtocol" json:"transport_protocol,omitempty"` // The name of the network service, following convention in RFC6335. Examples // are like http, telnet, ssh, etc. ServiceName string `protobuf:"bytes,3,opt,name=service_name,json=serviceName,proto3" json:"service_name,omitempty"` // The software that provides the service behind the port. Software *Software `protobuf:"bytes,4,opt,name=software,proto3" json:"software,omitempty"` // The complete set of versions of the software. VersionSet *VersionSet `protobuf:"bytes,5,opt,name=version_set,json=versionSet,proto3" json:"version_set,omitempty"` // Banners generated by the service. Banner []string `protobuf:"bytes,6,rep,name=banner,proto3" json:"banner,omitempty"` // Context information about this network service. ServiceContext *ServiceContext `protobuf:"bytes,7,opt,name=service_context,json=serviceContext,proto3" json:"service_context,omitempty"` // The detected Common Platform Enumeration (CPE) name for service, // in the uri binding representation, like: cpe:/a:openbsd:openssh:8.4p1 Cpes []string `protobuf:"bytes,8,rep,name=cpes,proto3" json:"cpes,omitempty"` // List of supported SSL versions (e.g. TLSv1, SSLv3, ...) on the service. SupportedSslVersions []string `protobuf:"bytes,9,rep,name=supported_ssl_versions,json=supportedSslVersions,proto3" json:"supported_ssl_versions,omitempty"` // List of supported HTTP methods (e.g. POST, GET, ...) on the service. SupportedHttpMethods []string `protobuf:"bytes,10,rep,name=supported_http_methods,json=supportedHttpMethods,proto3" json:"supported_http_methods,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *NetworkService) Reset() { *x = NetworkService{} mi := &file_network_service_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *NetworkService) String() string { return protoimpl.X.MessageStringOf(x) } func (*NetworkService) ProtoMessage() {} func (x *NetworkService) ProtoReflect() protoreflect.Message { mi := &file_network_service_proto_msgTypes[0] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use NetworkService.ProtoReflect.Descriptor instead. func (*NetworkService) Descriptor() ([]byte, []int) { return file_network_service_proto_rawDescGZIP(), []int{0} } func (x *NetworkService) GetNetworkEndpoint() *NetworkEndpoint { if x != nil { return x.NetworkEndpoint } return nil } func (x *NetworkService) GetTransportProtocol() TransportProtocol { if x != nil { return x.TransportProtocol } return TransportProtocol_TRANSPORT_PROTOCOL_UNSPECIFIED } func (x *NetworkService) GetServiceName() string { if x != nil { return x.ServiceName } return "" } func (x *NetworkService) GetSoftware() *Software { if x != nil { return x.Software } return nil } func (x *NetworkService) GetVersionSet() *VersionSet { if x != nil { return x.VersionSet } return nil } func (x *NetworkService) GetBanner() []string { if x != nil { return x.Banner } return nil } func (x *NetworkService) GetServiceContext() *ServiceContext { if x != nil { return x.ServiceContext } return nil } func (x *NetworkService) GetCpes() []string { if x != nil { return x.Cpes } return nil } func (x *NetworkService) GetSupportedSslVersions() []string { if x != nil { return x.SupportedSslVersions } return nil } func (x *NetworkService) GetSupportedHttpMethods() []string { if x != nil { return x.SupportedHttpMethods } return nil } // Context information about a specific network service. type ServiceContext struct { state protoimpl.MessageState `protogen:"open.v1"` // Types that are valid to be assigned to Context: // // *ServiceContext_WebServiceContext Context isServiceContext_Context `protobuf_oneof:"context"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *ServiceContext) Reset() { *x = ServiceContext{} mi := &file_network_service_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *ServiceContext) String() string { return protoimpl.X.MessageStringOf(x) } func (*ServiceContext) ProtoMessage() {} func (x *ServiceContext) ProtoReflect() protoreflect.Message { mi := &file_network_service_proto_msgTypes[1] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use ServiceContext.ProtoReflect.Descriptor instead. func (*ServiceContext) Descriptor() ([]byte, []int) { return file_network_service_proto_rawDescGZIP(), []int{1} } func (x *ServiceContext) GetContext() isServiceContext_Context { if x != nil { return x.Context } return nil } func (x *ServiceContext) GetWebServiceContext() *WebServiceContext { if x != nil { if x, ok := x.Context.(*ServiceContext_WebServiceContext); ok { return x.WebServiceContext } } return nil } type isServiceContext_Context interface { isServiceContext_Context() } type ServiceContext_WebServiceContext struct { WebServiceContext *WebServiceContext `protobuf:"bytes,1,opt,name=web_service_context,json=webServiceContext,proto3,oneof"` } func (*ServiceContext_WebServiceContext) isServiceContext_Context() {} // Context information about a web application. // NEXT ID: 5 type WebServiceContext struct { state protoimpl.MessageState `protogen:"open.v1"` // The root path of the hosted web application. ApplicationRoot string `protobuf:"bytes,1,opt,name=application_root,json=applicationRoot,proto3" json:"application_root,omitempty"` // The web application that is serving under the application root. Software *Software `protobuf:"bytes,2,opt,name=software,proto3" json:"software,omitempty"` // The detected versions of the web application. VersionSet *VersionSet `protobuf:"bytes,3,opt,name=version_set,json=versionSet,proto3" json:"version_set,omitempty"` // Fingerprinter's crawling results for this web service. CrawlResults []*CrawlResult `protobuf:"bytes,4,rep,name=crawl_results,json=crawlResults,proto3" json:"crawl_results,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *WebServiceContext) Reset() { *x = WebServiceContext{} mi := &file_network_service_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *WebServiceContext) String() string { return protoimpl.X.MessageStringOf(x) } func (*WebServiceContext) ProtoMessage() {} func (x *WebServiceContext) ProtoReflect() protoreflect.Message { mi := &file_network_service_proto_msgTypes[2] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use WebServiceContext.ProtoReflect.Descriptor instead. func (*WebServiceContext) Descriptor() ([]byte, []int) { return file_network_service_proto_rawDescGZIP(), []int{2} } func (x *WebServiceContext) GetApplicationRoot() string { if x != nil { return x.ApplicationRoot } return "" } func (x *WebServiceContext) GetSoftware() *Software { if x != nil { return x.Software } return nil } func (x *WebServiceContext) GetVersionSet() *VersionSet { if x != nil { return x.VersionSet } return nil } func (x *WebServiceContext) GetCrawlResults() []*CrawlResult { if x != nil { return x.CrawlResults } return nil } var File_network_service_proto protoreflect.FileDescriptor var file_network_service_proto_rawDesc = string([]byte{ 0x0a, 0x15, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0d, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x0d, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x0e, 0x73, 0x6f, 0x66, 0x74, 0x77, 0x61, 0x72, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x0f, 0x77, 0x65, 0x62, 0x5f, 0x63, 0x72, 0x61, 0x77, 0x6c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xa0, 0x04, 0x0a, 0x0e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x49, 0x0a, 0x10, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x5f, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x52, 0x0f, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x4f, 0x0a, 0x12, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x20, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x11, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x33, 0x0a, 0x08, 0x73, 0x6f, 0x66, 0x74, 0x77, 0x61, 0x72, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x53, 0x6f, 0x66, 0x74, 0x77, 0x61, 0x72, 0x65, 0x52, 0x08, 0x73, 0x6f, 0x66, 0x74, 0x77, 0x61, 0x72, 0x65, 0x12, 0x3a, 0x0a, 0x0b, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x73, 0x65, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x52, 0x0a, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x62, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x18, 0x06, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x62, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x12, 0x46, 0x0a, 0x0f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x52, 0x0e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x70, 0x65, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, 0x63, 0x70, 0x65, 0x73, 0x12, 0x34, 0x0a, 0x16, 0x73, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x65, 0x64, 0x5f, 0x73, 0x73, 0x6c, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x09, 0x20, 0x03, 0x28, 0x09, 0x52, 0x14, 0x73, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x65, 0x64, 0x53, 0x73, 0x6c, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x34, 0x0a, 0x16, 0x73, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x65, 0x64, 0x5f, 0x68, 0x74, 0x74, 0x70, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x09, 0x52, 0x14, 0x73, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x65, 0x64, 0x48, 0x74, 0x74, 0x70, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x22, 0x6f, 0x0a, 0x0e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x12, 0x52, 0x0a, 0x13, 0x77, 0x65, 0x62, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x57, 0x65, 0x62, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x48, 0x00, 0x52, 0x11, 0x77, 0x65, 0x62, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x42, 0x09, 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x22, 0xf0, 0x01, 0x0a, 0x11, 0x57, 0x65, 0x62, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x12, 0x29, 0x0a, 0x10, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x72, 0x6f, 0x6f, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x6f, 0x6f, 0x74, 0x12, 0x33, 0x0a, 0x08, 0x73, 0x6f, 0x66, 0x74, 0x77, 0x61, 0x72, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x53, 0x6f, 0x66, 0x74, 0x77, 0x61, 0x72, 0x65, 0x52, 0x08, 0x73, 0x6f, 0x66, 0x74, 0x77, 0x61, 0x72, 0x65, 0x12, 0x3a, 0x0a, 0x0b, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x73, 0x65, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x52, 0x0a, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x12, 0x3f, 0x0a, 0x0d, 0x63, 0x72, 0x61, 0x77, 0x6c, 0x5f, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x43, 0x72, 0x61, 0x77, 0x6c, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x0c, 0x63, 0x72, 0x61, 0x77, 0x6c, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x42, 0x75, 0x0a, 0x18, 0x63, 0x6f, 0x6d, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x42, 0x14, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x50, 0x01, 0x5a, 0x41, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2d, 0x73, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x2d, 0x73, 0x63, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x5f, 0x67, 0x6f, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, }) var ( file_network_service_proto_rawDescOnce sync.Once file_network_service_proto_rawDescData []byte ) func file_network_service_proto_rawDescGZIP() []byte { file_network_service_proto_rawDescOnce.Do(func() { file_network_service_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_network_service_proto_rawDesc), len(file_network_service_proto_rawDesc))) }) return file_network_service_proto_rawDescData } var file_network_service_proto_msgTypes = make([]protoimpl.MessageInfo, 3) var file_network_service_proto_goTypes = []any{ (*NetworkService)(nil), // 0: tsunami.proto.NetworkService (*ServiceContext)(nil), // 1: tsunami.proto.ServiceContext (*WebServiceContext)(nil), // 2: tsunami.proto.WebServiceContext (*NetworkEndpoint)(nil), // 3: tsunami.proto.NetworkEndpoint (TransportProtocol)(0), // 4: tsunami.proto.TransportProtocol (*Software)(nil), // 5: tsunami.proto.Software (*VersionSet)(nil), // 6: tsunami.proto.VersionSet (*CrawlResult)(nil), // 7: tsunami.proto.CrawlResult } var file_network_service_proto_depIdxs = []int32{ 3, // 0: tsunami.proto.NetworkService.network_endpoint:type_name -> tsunami.proto.NetworkEndpoint 4, // 1: tsunami.proto.NetworkService.transport_protocol:type_name -> tsunami.proto.TransportProtocol 5, // 2: tsunami.proto.NetworkService.software:type_name -> tsunami.proto.Software 6, // 3: tsunami.proto.NetworkService.version_set:type_name -> tsunami.proto.VersionSet 1, // 4: tsunami.proto.NetworkService.service_context:type_name -> tsunami.proto.ServiceContext 2, // 5: tsunami.proto.ServiceContext.web_service_context:type_name -> tsunami.proto.WebServiceContext 5, // 6: tsunami.proto.WebServiceContext.software:type_name -> tsunami.proto.Software 6, // 7: tsunami.proto.WebServiceContext.version_set:type_name -> tsunami.proto.VersionSet 7, // 8: tsunami.proto.WebServiceContext.crawl_results:type_name -> tsunami.proto.CrawlResult 9, // [9:9] is the sub-list for method output_type 9, // [9:9] is the sub-list for method input_type 9, // [9:9] is the sub-list for extension type_name 9, // [9:9] is the sub-list for extension extendee 0, // [0:9] is the sub-list for field type_name } func init() { file_network_service_proto_init() } func file_network_service_proto_init() { if File_network_service_proto != nil { return } file_network_proto_init() file_software_proto_init() file_web_crawl_proto_init() file_network_service_proto_msgTypes[1].OneofWrappers = []any{ (*ServiceContext_WebServiceContext)(nil), } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_network_service_proto_rawDesc), len(file_network_service_proto_rawDesc)), NumEnums: 0, NumMessages: 3, NumExtensions: 0, NumServices: 0, }, GoTypes: file_network_service_proto_goTypes, DependencyIndexes: file_network_service_proto_depIdxs, MessageInfos: file_network_service_proto_msgTypes, }.Build() File_network_service_proto = out.File file_network_service_proto_goTypes = nil file_network_service_proto_depIdxs = nil } ================================================ FILE: proto/tsunami_go_proto/payload_generator.pb.go ================================================ // // Copyright 2022 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Data models utilized by the Tsunami Paylaod Generator // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.5 // protoc v3.21.12 // source: payload_generator.proto package tsunami_go_proto import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" wrapperspb "google.golang.org/protobuf/types/known/wrapperspb" reflect "reflect" sync "sync" unsafe "unsafe" ) const ( // Verify that this generated code is sufficiently up-to-date. _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) // Verify that runtime/protoimpl is sufficiently up-to-date. _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) type PayloadValidationType int32 const ( PayloadValidationType_VALIDATION_TYPE_UNSPECIFIED PayloadValidationType = 0 PayloadValidationType_VALIDATION_REGEX PayloadValidationType = 1 ) // Enum value maps for PayloadValidationType. var ( PayloadValidationType_name = map[int32]string{ 0: "VALIDATION_TYPE_UNSPECIFIED", 1: "VALIDATION_REGEX", } PayloadValidationType_value = map[string]int32{ "VALIDATION_TYPE_UNSPECIFIED": 0, "VALIDATION_REGEX": 1, } ) func (x PayloadValidationType) Enum() *PayloadValidationType { p := new(PayloadValidationType) *p = x return p } func (x PayloadValidationType) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (PayloadValidationType) Descriptor() protoreflect.EnumDescriptor { return file_payload_generator_proto_enumTypes[0].Descriptor() } func (PayloadValidationType) Type() protoreflect.EnumType { return &file_payload_generator_proto_enumTypes[0] } func (x PayloadValidationType) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } // Deprecated: Use PayloadValidationType.Descriptor instead. func (PayloadValidationType) EnumDescriptor() ([]byte, []int) { return file_payload_generator_proto_rawDescGZIP(), []int{0} } // The type of vulnerability the detector is looking for type PayloadGeneratorConfig_VulnerabilityType int32 const ( // Unspecified vulnerability type PayloadGeneratorConfig_VULNERABILITY_TYPE_UNSPECIFIED PayloadGeneratorConfig_VulnerabilityType = 0 // RCE which returns the output of the execution PayloadGeneratorConfig_REFLECTIVE_RCE PayloadGeneratorConfig_VulnerabilityType = 1 // RCE which does not return the output of the execution PayloadGeneratorConfig_BLIND_RCE PayloadGeneratorConfig_VulnerabilityType = 2 // Server-Side Request Forgery PayloadGeneratorConfig_SSRF PayloadGeneratorConfig_VulnerabilityType = 3 // Arbitrary File Write PayloadGeneratorConfig_ARBITRARY_FILE_WRITE PayloadGeneratorConfig_VulnerabilityType = 4 // RCE without output of the execution + File Read (needed to get // confirmation string) PayloadGeneratorConfig_BLIND_RCE_FILE_READ PayloadGeneratorConfig_VulnerabilityType = 5 ) // Enum value maps for PayloadGeneratorConfig_VulnerabilityType. var ( PayloadGeneratorConfig_VulnerabilityType_name = map[int32]string{ 0: "VULNERABILITY_TYPE_UNSPECIFIED", 1: "REFLECTIVE_RCE", 2: "BLIND_RCE", 3: "SSRF", 4: "ARBITRARY_FILE_WRITE", 5: "BLIND_RCE_FILE_READ", } PayloadGeneratorConfig_VulnerabilityType_value = map[string]int32{ "VULNERABILITY_TYPE_UNSPECIFIED": 0, "REFLECTIVE_RCE": 1, "BLIND_RCE": 2, "SSRF": 3, "ARBITRARY_FILE_WRITE": 4, "BLIND_RCE_FILE_READ": 5, } ) func (x PayloadGeneratorConfig_VulnerabilityType) Enum() *PayloadGeneratorConfig_VulnerabilityType { p := new(PayloadGeneratorConfig_VulnerabilityType) *p = x return p } func (x PayloadGeneratorConfig_VulnerabilityType) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (PayloadGeneratorConfig_VulnerabilityType) Descriptor() protoreflect.EnumDescriptor { return file_payload_generator_proto_enumTypes[1].Descriptor() } func (PayloadGeneratorConfig_VulnerabilityType) Type() protoreflect.EnumType { return &file_payload_generator_proto_enumTypes[1] } func (x PayloadGeneratorConfig_VulnerabilityType) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } // Deprecated: Use PayloadGeneratorConfig_VulnerabilityType.Descriptor instead. func (PayloadGeneratorConfig_VulnerabilityType) EnumDescriptor() ([]byte, []int) { return file_payload_generator_proto_rawDescGZIP(), []int{0, 0} } // The environment that processes the payload for execution e.g. a PHP-based // target likely wants a payload that is itself PHP code. type PayloadGeneratorConfig_InterpretationEnvironment int32 const ( // Unspecified interpretation environment type PayloadGeneratorConfig_INTERPRETATION_ENVIRONMENT_UNSPECIFIED PayloadGeneratorConfig_InterpretationEnvironment = 0 // Payload is interpreted within a Linux shell environment PayloadGeneratorConfig_LINUX_SHELL PayloadGeneratorConfig_InterpretationEnvironment = 1 // Payload is interpreted wihin a Java compiler context PayloadGeneratorConfig_JAVA PayloadGeneratorConfig_InterpretationEnvironment = 2 // Payload is interpreted wihin a PHP VM context PayloadGeneratorConfig_PHP PayloadGeneratorConfig_InterpretationEnvironment = 3 // Interpretation environment doesn't matter PayloadGeneratorConfig_INTERPRETATION_ANY PayloadGeneratorConfig_InterpretationEnvironment = 4 // Payload is interpreted wihin crontab PayloadGeneratorConfig_LINUX_ROOT_CRONTAB PayloadGeneratorConfig_InterpretationEnvironment = 5 // Payload is interpreted wihin a Windows shell environment PayloadGeneratorConfig_WINDOWS_SHELL PayloadGeneratorConfig_InterpretationEnvironment = 6 // Payload is interpreted within a JSP shell environment PayloadGeneratorConfig_JSP PayloadGeneratorConfig_InterpretationEnvironment = 7 ) // Enum value maps for PayloadGeneratorConfig_InterpretationEnvironment. var ( PayloadGeneratorConfig_InterpretationEnvironment_name = map[int32]string{ 0: "INTERPRETATION_ENVIRONMENT_UNSPECIFIED", 1: "LINUX_SHELL", 2: "JAVA", 3: "PHP", 4: "INTERPRETATION_ANY", 5: "LINUX_ROOT_CRONTAB", 6: "WINDOWS_SHELL", 7: "JSP", } PayloadGeneratorConfig_InterpretationEnvironment_value = map[string]int32{ "INTERPRETATION_ENVIRONMENT_UNSPECIFIED": 0, "LINUX_SHELL": 1, "JAVA": 2, "PHP": 3, "INTERPRETATION_ANY": 4, "LINUX_ROOT_CRONTAB": 5, "WINDOWS_SHELL": 6, "JSP": 7, } ) func (x PayloadGeneratorConfig_InterpretationEnvironment) Enum() *PayloadGeneratorConfig_InterpretationEnvironment { p := new(PayloadGeneratorConfig_InterpretationEnvironment) *p = x return p } func (x PayloadGeneratorConfig_InterpretationEnvironment) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (PayloadGeneratorConfig_InterpretationEnvironment) Descriptor() protoreflect.EnumDescriptor { return file_payload_generator_proto_enumTypes[2].Descriptor() } func (PayloadGeneratorConfig_InterpretationEnvironment) Type() protoreflect.EnumType { return &file_payload_generator_proto_enumTypes[2] } func (x PayloadGeneratorConfig_InterpretationEnvironment) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } // Deprecated: Use PayloadGeneratorConfig_InterpretationEnvironment.Descriptor instead. func (PayloadGeneratorConfig_InterpretationEnvironment) EnumDescriptor() ([]byte, []int) { return file_payload_generator_proto_rawDescGZIP(), []int{0, 1} } // The actual runtime environment when the payload is run e.g. while a // PHP-based target wants a PHP-interpretation environment, the actual code // execution may happen via the Linux shell: exec(“echo \”this is running in // the system.\””). type PayloadGeneratorConfig_ExecutionEnvironment int32 const ( // Unspecified execution environment type PayloadGeneratorConfig_EXECUTION_ENVIRONMENT_UNSPECIFIED PayloadGeneratorConfig_ExecutionEnvironment = 0 // Execute within the InterpretationEnvironment PayloadGeneratorConfig_EXEC_INTERPRETATION_ENVIRONMENT PayloadGeneratorConfig_ExecutionEnvironment = 1 // Execution environment doesn't matter PayloadGeneratorConfig_EXEC_ANY PayloadGeneratorConfig_ExecutionEnvironment = 2 ) // Enum value maps for PayloadGeneratorConfig_ExecutionEnvironment. var ( PayloadGeneratorConfig_ExecutionEnvironment_name = map[int32]string{ 0: "EXECUTION_ENVIRONMENT_UNSPECIFIED", 1: "EXEC_INTERPRETATION_ENVIRONMENT", 2: "EXEC_ANY", } PayloadGeneratorConfig_ExecutionEnvironment_value = map[string]int32{ "EXECUTION_ENVIRONMENT_UNSPECIFIED": 0, "EXEC_INTERPRETATION_ENVIRONMENT": 1, "EXEC_ANY": 2, } ) func (x PayloadGeneratorConfig_ExecutionEnvironment) Enum() *PayloadGeneratorConfig_ExecutionEnvironment { p := new(PayloadGeneratorConfig_ExecutionEnvironment) *p = x return p } func (x PayloadGeneratorConfig_ExecutionEnvironment) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (PayloadGeneratorConfig_ExecutionEnvironment) Descriptor() protoreflect.EnumDescriptor { return file_payload_generator_proto_enumTypes[3].Descriptor() } func (PayloadGeneratorConfig_ExecutionEnvironment) Type() protoreflect.EnumType { return &file_payload_generator_proto_enumTypes[3] } func (x PayloadGeneratorConfig_ExecutionEnvironment) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } // Deprecated: Use PayloadGeneratorConfig_ExecutionEnvironment.Descriptor instead. func (PayloadGeneratorConfig_ExecutionEnvironment) EnumDescriptor() ([]byte, []int) { return file_payload_generator_proto_rawDescGZIP(), []int{0, 2} } // Attributes utilized by the PayloadGenerator to select a payload type PayloadGeneratorConfig struct { state protoimpl.MessageState `protogen:"open.v1"` VulnerabilityType PayloadGeneratorConfig_VulnerabilityType `protobuf:"varint,2,opt,name=vulnerability_type,json=vulnerabilityType,proto3,enum=tsunami.proto.PayloadGeneratorConfig_VulnerabilityType" json:"vulnerability_type,omitempty"` InterpretationEnvironment PayloadGeneratorConfig_InterpretationEnvironment `protobuf:"varint,3,opt,name=interpretation_environment,json=interpretationEnvironment,proto3,enum=tsunami.proto.PayloadGeneratorConfig_InterpretationEnvironment" json:"interpretation_environment,omitempty"` ExecutionEnvironment PayloadGeneratorConfig_ExecutionEnvironment `protobuf:"varint,4,opt,name=execution_environment,json=executionEnvironment,proto3,enum=tsunami.proto.PayloadGeneratorConfig_ExecutionEnvironment" json:"execution_environment,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *PayloadGeneratorConfig) Reset() { *x = PayloadGeneratorConfig{} mi := &file_payload_generator_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *PayloadGeneratorConfig) String() string { return protoimpl.X.MessageStringOf(x) } func (*PayloadGeneratorConfig) ProtoMessage() {} func (x *PayloadGeneratorConfig) ProtoReflect() protoreflect.Message { mi := &file_payload_generator_proto_msgTypes[0] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use PayloadGeneratorConfig.ProtoReflect.Descriptor instead. func (*PayloadGeneratorConfig) Descriptor() ([]byte, []int) { return file_payload_generator_proto_rawDescGZIP(), []int{0} } func (x *PayloadGeneratorConfig) GetVulnerabilityType() PayloadGeneratorConfig_VulnerabilityType { if x != nil { return x.VulnerabilityType } return PayloadGeneratorConfig_VULNERABILITY_TYPE_UNSPECIFIED } func (x *PayloadGeneratorConfig) GetInterpretationEnvironment() PayloadGeneratorConfig_InterpretationEnvironment { if x != nil { return x.InterpretationEnvironment } return PayloadGeneratorConfig_INTERPRETATION_ENVIRONMENT_UNSPECIFIED } func (x *PayloadGeneratorConfig) GetExecutionEnvironment() PayloadGeneratorConfig_ExecutionEnvironment { if x != nil { return x.ExecutionEnvironment } return PayloadGeneratorConfig_EXECUTION_ENVIRONMENT_UNSPECIFIED } // Attributes of a payload. A detector can check these attributes to change its // logic based on the payload type. type PayloadAttributes struct { state protoimpl.MessageState `protogen:"open.v1"` // Whether the payload uses the callback server UsesCallbackServer bool `protobuf:"varint,1,opt,name=uses_callback_server,json=usesCallbackServer,proto3" json:"uses_callback_server,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *PayloadAttributes) Reset() { *x = PayloadAttributes{} mi := &file_payload_generator_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *PayloadAttributes) String() string { return protoimpl.X.MessageStringOf(x) } func (*PayloadAttributes) ProtoMessage() {} func (x *PayloadAttributes) ProtoReflect() protoreflect.Message { mi := &file_payload_generator_proto_msgTypes[1] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use PayloadAttributes.ProtoReflect.Descriptor instead. func (*PayloadAttributes) Descriptor() ([]byte, []int) { return file_payload_generator_proto_rawDescGZIP(), []int{1} } func (x *PayloadAttributes) GetUsesCallbackServer() bool { if x != nil { return x.UsesCallbackServer } return false } // Container type for payload_definitions.yaml type PayloadLibrary struct { state protoimpl.MessageState `protogen:"open.v1"` Payloads []*PayloadDefinition `protobuf:"bytes,1,rep,name=payloads,proto3" json:"payloads,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *PayloadLibrary) Reset() { *x = PayloadLibrary{} mi := &file_payload_generator_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *PayloadLibrary) String() string { return protoimpl.X.MessageStringOf(x) } func (*PayloadLibrary) ProtoMessage() {} func (x *PayloadLibrary) ProtoReflect() protoreflect.Message { mi := &file_payload_generator_proto_msgTypes[2] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use PayloadLibrary.ProtoReflect.Descriptor instead. func (*PayloadLibrary) Descriptor() ([]byte, []int) { return file_payload_generator_proto_rawDescGZIP(), []int{2} } func (x *PayloadLibrary) GetPayloads() []*PayloadDefinition { if x != nil { return x.Payloads } return nil } // Schema for each entry in payload_definitions.yaml // Note: this message uses StringValue and BoolValue because we validate whether // each payload definition in the yaml file has the correct fields present. // Since empty proto fields are given default values (proto fields are not // nullable), we use the wrapped types to check for actual presence. type PayloadDefinition struct { state protoimpl.MessageState `protogen:"open.v1"` // The human-readable string to identify the payload Name *wrapperspb.StringValue `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` InterpretationEnvironment PayloadGeneratorConfig_InterpretationEnvironment `protobuf:"varint,2,opt,name=interpretation_environment,json=interpretationEnvironment,proto3,enum=tsunami.proto.PayloadGeneratorConfig_InterpretationEnvironment" json:"interpretation_environment,omitempty"` ExecutionEnvironment PayloadGeneratorConfig_ExecutionEnvironment `protobuf:"varint,3,opt,name=execution_environment,json=executionEnvironment,proto3,enum=tsunami.proto.PayloadGeneratorConfig_ExecutionEnvironment" json:"execution_environment,omitempty"` // All vulnerability types this payload can be used for VulnerabilityType []PayloadGeneratorConfig_VulnerabilityType `protobuf:"varint,4,rep,packed,name=vulnerability_type,json=vulnerabilityType,proto3,enum=tsunami.proto.PayloadGeneratorConfig_VulnerabilityType" json:"vulnerability_type,omitempty"` // If true, payload_string must contain the $TSUNAMI_PAYLOAD_TOKEN_URL // token. Validation will automatically check against the callback server, so // the validation* fields do not need to be set. UsesCallbackServer *wrapperspb.BoolValue `protobuf:"bytes,5,opt,name=uses_callback_server,json=usesCallbackServer,proto3" json:"uses_callback_server,omitempty"` // The actual payload command string. The following special tokens can be // used which will cause the framework to inject dynamic content into the // command: // - $TSUNAMI_PAYLOAD_TOKEN_URL: url for the callback server // - a random string, used to reduce false positives. PayloadString *wrapperspb.StringValue `protobuf:"bytes,6,opt,name=payload_string,json=payloadString,proto3" json:"payload_string,omitempty"` // The type of validation function for determining if the payload was // executed. Currently, only REGEX is supported. ValidationType PayloadValidationType `protobuf:"varint,7,opt,name=validation_type,json=validationType,proto3,enum=tsunami.proto.PayloadValidationType" json:"validation_type,omitempty"` // Required if validation_type == REGEX. Must be compatible with // java.util.regex.Pattern. The string will first be preprocessed before // applied as a regex, replacing any of the following tokens with the // corresponding values supplied by the framework: // - $TSUNAMI_PAYLOAD_TOKEN_RANDOM: a random string, used to reduce false // positives. The value is guaranteed to be the same as the value supplied // to payload_string. ValidationRegex *wrapperspb.StringValue `protobuf:"bytes,8,opt,name=validation_regex,json=validationRegex,proto3" json:"validation_regex,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *PayloadDefinition) Reset() { *x = PayloadDefinition{} mi := &file_payload_generator_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *PayloadDefinition) String() string { return protoimpl.X.MessageStringOf(x) } func (*PayloadDefinition) ProtoMessage() {} func (x *PayloadDefinition) ProtoReflect() protoreflect.Message { mi := &file_payload_generator_proto_msgTypes[3] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use PayloadDefinition.ProtoReflect.Descriptor instead. func (*PayloadDefinition) Descriptor() ([]byte, []int) { return file_payload_generator_proto_rawDescGZIP(), []int{3} } func (x *PayloadDefinition) GetName() *wrapperspb.StringValue { if x != nil { return x.Name } return nil } func (x *PayloadDefinition) GetInterpretationEnvironment() PayloadGeneratorConfig_InterpretationEnvironment { if x != nil { return x.InterpretationEnvironment } return PayloadGeneratorConfig_INTERPRETATION_ENVIRONMENT_UNSPECIFIED } func (x *PayloadDefinition) GetExecutionEnvironment() PayloadGeneratorConfig_ExecutionEnvironment { if x != nil { return x.ExecutionEnvironment } return PayloadGeneratorConfig_EXECUTION_ENVIRONMENT_UNSPECIFIED } func (x *PayloadDefinition) GetVulnerabilityType() []PayloadGeneratorConfig_VulnerabilityType { if x != nil { return x.VulnerabilityType } return nil } func (x *PayloadDefinition) GetUsesCallbackServer() *wrapperspb.BoolValue { if x != nil { return x.UsesCallbackServer } return nil } func (x *PayloadDefinition) GetPayloadString() *wrapperspb.StringValue { if x != nil { return x.PayloadString } return nil } func (x *PayloadDefinition) GetValidationType() PayloadValidationType { if x != nil { return x.ValidationType } return PayloadValidationType_VALIDATION_TYPE_UNSPECIFIED } func (x *PayloadDefinition) GetValidationRegex() *wrapperspb.StringValue { if x != nil { return x.ValidationRegex } return nil } var File_payload_generator_proto protoreflect.FileDescriptor var file_payload_generator_proto_rawDesc = string([]byte{ 0x0a, 0x17, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x5f, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0d, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x77, 0x72, 0x61, 0x70, 0x70, 0x65, 0x72, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xb7, 0x06, 0x0a, 0x16, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x66, 0x0a, 0x12, 0x76, 0x75, 0x6c, 0x6e, 0x65, 0x72, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x37, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x56, 0x75, 0x6c, 0x6e, 0x65, 0x72, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x54, 0x79, 0x70, 0x65, 0x52, 0x11, 0x76, 0x75, 0x6c, 0x6e, 0x65, 0x72, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x54, 0x79, 0x70, 0x65, 0x12, 0x7e, 0x0a, 0x1a, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x65, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x3f, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x65, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x19, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x65, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x6f, 0x0a, 0x15, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x3a, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x14, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x22, 0x97, 0x01, 0x0a, 0x11, 0x56, 0x75, 0x6c, 0x6e, 0x65, 0x72, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x54, 0x79, 0x70, 0x65, 0x12, 0x22, 0x0a, 0x1e, 0x56, 0x55, 0x4c, 0x4e, 0x45, 0x52, 0x41, 0x42, 0x49, 0x4c, 0x49, 0x54, 0x59, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x12, 0x0a, 0x0e, 0x52, 0x45, 0x46, 0x4c, 0x45, 0x43, 0x54, 0x49, 0x56, 0x45, 0x5f, 0x52, 0x43, 0x45, 0x10, 0x01, 0x12, 0x0d, 0x0a, 0x09, 0x42, 0x4c, 0x49, 0x4e, 0x44, 0x5f, 0x52, 0x43, 0x45, 0x10, 0x02, 0x12, 0x08, 0x0a, 0x04, 0x53, 0x53, 0x52, 0x46, 0x10, 0x03, 0x12, 0x18, 0x0a, 0x14, 0x41, 0x52, 0x42, 0x49, 0x54, 0x52, 0x41, 0x52, 0x59, 0x5f, 0x46, 0x49, 0x4c, 0x45, 0x5f, 0x57, 0x52, 0x49, 0x54, 0x45, 0x10, 0x04, 0x12, 0x17, 0x0a, 0x13, 0x42, 0x4c, 0x49, 0x4e, 0x44, 0x5f, 0x52, 0x43, 0x45, 0x5f, 0x46, 0x49, 0x4c, 0x45, 0x5f, 0x52, 0x45, 0x41, 0x44, 0x10, 0x05, 0x22, 0xb7, 0x01, 0x0a, 0x19, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x65, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x2a, 0x0a, 0x26, 0x49, 0x4e, 0x54, 0x45, 0x52, 0x50, 0x52, 0x45, 0x54, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x45, 0x4e, 0x56, 0x49, 0x52, 0x4f, 0x4e, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0f, 0x0a, 0x0b, 0x4c, 0x49, 0x4e, 0x55, 0x58, 0x5f, 0x53, 0x48, 0x45, 0x4c, 0x4c, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x4a, 0x41, 0x56, 0x41, 0x10, 0x02, 0x12, 0x07, 0x0a, 0x03, 0x50, 0x48, 0x50, 0x10, 0x03, 0x12, 0x16, 0x0a, 0x12, 0x49, 0x4e, 0x54, 0x45, 0x52, 0x50, 0x52, 0x45, 0x54, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x41, 0x4e, 0x59, 0x10, 0x04, 0x12, 0x16, 0x0a, 0x12, 0x4c, 0x49, 0x4e, 0x55, 0x58, 0x5f, 0x52, 0x4f, 0x4f, 0x54, 0x5f, 0x43, 0x52, 0x4f, 0x4e, 0x54, 0x41, 0x42, 0x10, 0x05, 0x12, 0x11, 0x0a, 0x0d, 0x57, 0x49, 0x4e, 0x44, 0x4f, 0x57, 0x53, 0x5f, 0x53, 0x48, 0x45, 0x4c, 0x4c, 0x10, 0x06, 0x12, 0x07, 0x0a, 0x03, 0x4a, 0x53, 0x50, 0x10, 0x07, 0x22, 0x70, 0x0a, 0x14, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x25, 0x0a, 0x21, 0x45, 0x58, 0x45, 0x43, 0x55, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x45, 0x4e, 0x56, 0x49, 0x52, 0x4f, 0x4e, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x23, 0x0a, 0x1f, 0x45, 0x58, 0x45, 0x43, 0x5f, 0x49, 0x4e, 0x54, 0x45, 0x52, 0x50, 0x52, 0x45, 0x54, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x45, 0x4e, 0x56, 0x49, 0x52, 0x4f, 0x4e, 0x4d, 0x45, 0x4e, 0x54, 0x10, 0x01, 0x12, 0x0c, 0x0a, 0x08, 0x45, 0x58, 0x45, 0x43, 0x5f, 0x41, 0x4e, 0x59, 0x10, 0x02, 0x22, 0x45, 0x0a, 0x11, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x30, 0x0a, 0x14, 0x75, 0x73, 0x65, 0x73, 0x5f, 0x63, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x12, 0x75, 0x73, 0x65, 0x73, 0x43, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x22, 0x4e, 0x0a, 0x0e, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x4c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x79, 0x12, 0x3c, 0x0a, 0x08, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x44, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x08, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x73, 0x22, 0xc9, 0x05, 0x0a, 0x11, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x44, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x30, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x7e, 0x0a, 0x1a, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x65, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x3f, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x65, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x19, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x65, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x6f, 0x0a, 0x15, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x3a, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x14, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x66, 0x0a, 0x12, 0x76, 0x75, 0x6c, 0x6e, 0x65, 0x72, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0e, 0x32, 0x37, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x56, 0x75, 0x6c, 0x6e, 0x65, 0x72, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x54, 0x79, 0x70, 0x65, 0x52, 0x11, 0x76, 0x75, 0x6c, 0x6e, 0x65, 0x72, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x54, 0x79, 0x70, 0x65, 0x12, 0x4c, 0x0a, 0x14, 0x75, 0x73, 0x65, 0x73, 0x5f, 0x63, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x12, 0x75, 0x73, 0x65, 0x73, 0x43, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x43, 0x0a, 0x0e, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x5f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0d, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x12, 0x4d, 0x0a, 0x0f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x24, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x52, 0x0e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x12, 0x47, 0x0a, 0x10, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x72, 0x65, 0x67, 0x65, 0x78, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x67, 0x65, 0x78, 0x2a, 0x4e, 0x0a, 0x15, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1f, 0x0a, 0x1b, 0x56, 0x41, 0x4c, 0x49, 0x44, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x14, 0x0a, 0x10, 0x56, 0x41, 0x4c, 0x49, 0x44, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x52, 0x45, 0x47, 0x45, 0x58, 0x10, 0x01, 0x42, 0x77, 0x0a, 0x18, 0x63, 0x6f, 0x6d, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x42, 0x16, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x50, 0x01, 0x5a, 0x41, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2d, 0x73, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x2d, 0x73, 0x63, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x5f, 0x67, 0x6f, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, }) var ( file_payload_generator_proto_rawDescOnce sync.Once file_payload_generator_proto_rawDescData []byte ) func file_payload_generator_proto_rawDescGZIP() []byte { file_payload_generator_proto_rawDescOnce.Do(func() { file_payload_generator_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_payload_generator_proto_rawDesc), len(file_payload_generator_proto_rawDesc))) }) return file_payload_generator_proto_rawDescData } var file_payload_generator_proto_enumTypes = make([]protoimpl.EnumInfo, 4) var file_payload_generator_proto_msgTypes = make([]protoimpl.MessageInfo, 4) var file_payload_generator_proto_goTypes = []any{ (PayloadValidationType)(0), // 0: tsunami.proto.PayloadValidationType (PayloadGeneratorConfig_VulnerabilityType)(0), // 1: tsunami.proto.PayloadGeneratorConfig.VulnerabilityType (PayloadGeneratorConfig_InterpretationEnvironment)(0), // 2: tsunami.proto.PayloadGeneratorConfig.InterpretationEnvironment (PayloadGeneratorConfig_ExecutionEnvironment)(0), // 3: tsunami.proto.PayloadGeneratorConfig.ExecutionEnvironment (*PayloadGeneratorConfig)(nil), // 4: tsunami.proto.PayloadGeneratorConfig (*PayloadAttributes)(nil), // 5: tsunami.proto.PayloadAttributes (*PayloadLibrary)(nil), // 6: tsunami.proto.PayloadLibrary (*PayloadDefinition)(nil), // 7: tsunami.proto.PayloadDefinition (*wrapperspb.StringValue)(nil), // 8: google.protobuf.StringValue (*wrapperspb.BoolValue)(nil), // 9: google.protobuf.BoolValue } var file_payload_generator_proto_depIdxs = []int32{ 1, // 0: tsunami.proto.PayloadGeneratorConfig.vulnerability_type:type_name -> tsunami.proto.PayloadGeneratorConfig.VulnerabilityType 2, // 1: tsunami.proto.PayloadGeneratorConfig.interpretation_environment:type_name -> tsunami.proto.PayloadGeneratorConfig.InterpretationEnvironment 3, // 2: tsunami.proto.PayloadGeneratorConfig.execution_environment:type_name -> tsunami.proto.PayloadGeneratorConfig.ExecutionEnvironment 7, // 3: tsunami.proto.PayloadLibrary.payloads:type_name -> tsunami.proto.PayloadDefinition 8, // 4: tsunami.proto.PayloadDefinition.name:type_name -> google.protobuf.StringValue 2, // 5: tsunami.proto.PayloadDefinition.interpretation_environment:type_name -> tsunami.proto.PayloadGeneratorConfig.InterpretationEnvironment 3, // 6: tsunami.proto.PayloadDefinition.execution_environment:type_name -> tsunami.proto.PayloadGeneratorConfig.ExecutionEnvironment 1, // 7: tsunami.proto.PayloadDefinition.vulnerability_type:type_name -> tsunami.proto.PayloadGeneratorConfig.VulnerabilityType 9, // 8: tsunami.proto.PayloadDefinition.uses_callback_server:type_name -> google.protobuf.BoolValue 8, // 9: tsunami.proto.PayloadDefinition.payload_string:type_name -> google.protobuf.StringValue 0, // 10: tsunami.proto.PayloadDefinition.validation_type:type_name -> tsunami.proto.PayloadValidationType 8, // 11: tsunami.proto.PayloadDefinition.validation_regex:type_name -> google.protobuf.StringValue 12, // [12:12] is the sub-list for method output_type 12, // [12:12] is the sub-list for method input_type 12, // [12:12] is the sub-list for extension type_name 12, // [12:12] is the sub-list for extension extendee 0, // [0:12] is the sub-list for field type_name } func init() { file_payload_generator_proto_init() } func file_payload_generator_proto_init() { if File_payload_generator_proto != nil { return } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_payload_generator_proto_rawDesc), len(file_payload_generator_proto_rawDesc)), NumEnums: 4, NumMessages: 4, NumExtensions: 0, NumServices: 0, }, GoTypes: file_payload_generator_proto_goTypes, DependencyIndexes: file_payload_generator_proto_depIdxs, EnumInfos: file_payload_generator_proto_enumTypes, MessageInfos: file_payload_generator_proto_msgTypes, }.Build() File_payload_generator_proto = out.File file_payload_generator_proto_goTypes = nil file_payload_generator_proto_depIdxs = nil } ================================================ FILE: proto/tsunami_go_proto/plugin_representation.pb.go ================================================ // // Copyright 2020 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Representation of a tsunami plugin definition passed between language // servers. // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.5 // protoc v3.21.12 // source: plugin_representation.proto package tsunami_go_proto import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" unsafe "unsafe" ) const ( // Verify that this generated code is sufficiently up-to-date. _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) // Verify that runtime/protoimpl is sufficiently up-to-date. _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) type PluginInfo_PluginType int32 const ( // Plugin is an unspecified type. PluginInfo_PLUGIN_TYPE_UNSPECIFIED PluginInfo_PluginType = 0 // Plugin is a port scanner. PluginInfo_PORT_SCAN PluginInfo_PluginType = 1 // Plugin is a service fingerprinter. PluginInfo_SERVICE_FINGERPRINT PluginInfo_PluginType = 2 // Plugin is a vulnerability detector. PluginInfo_VULN_DETECTION PluginInfo_PluginType = 3 ) // Enum value maps for PluginInfo_PluginType. var ( PluginInfo_PluginType_name = map[int32]string{ 0: "PLUGIN_TYPE_UNSPECIFIED", 1: "PORT_SCAN", 2: "SERVICE_FINGERPRINT", 3: "VULN_DETECTION", } PluginInfo_PluginType_value = map[string]int32{ "PLUGIN_TYPE_UNSPECIFIED": 0, "PORT_SCAN": 1, "SERVICE_FINGERPRINT": 2, "VULN_DETECTION": 3, } ) func (x PluginInfo_PluginType) Enum() *PluginInfo_PluginType { p := new(PluginInfo_PluginType) *p = x return p } func (x PluginInfo_PluginType) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (PluginInfo_PluginType) Descriptor() protoreflect.EnumDescriptor { return file_plugin_representation_proto_enumTypes[0].Descriptor() } func (PluginInfo_PluginType) Type() protoreflect.EnumType { return &file_plugin_representation_proto_enumTypes[0] } func (x PluginInfo_PluginType) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } // Deprecated: Use PluginInfo_PluginType.Descriptor instead. func (PluginInfo_PluginType) EnumDescriptor() ([]byte, []int) { return file_plugin_representation_proto_rawDescGZIP(), []int{1, 0} } // Represents a PluginDefinition placeholder. type PluginDefinition struct { state protoimpl.MessageState `protogen:"open.v1"` // PluginInfo of this definition. Info *PluginInfo `protobuf:"bytes,1,opt,name=info,proto3" json:"info,omitempty"` // The name of the target service. TargetServiceName *TargetServiceName `protobuf:"bytes,2,opt,name=target_service_name,json=targetServiceName,proto3" json:"target_service_name,omitempty"` // The name of the target software. TargetSoftware *TargetSoftware `protobuf:"bytes,3,opt,name=target_software,json=targetSoftware,proto3" json:"target_software,omitempty"` // If the definition is for a web service or not. ForWebService bool `protobuf:"varint,4,opt,name=for_web_service,json=forWebService,proto3" json:"for_web_service,omitempty"` // If the definition is for a specific operating system or not. // Note: this filter is executed within an AND condition with the other // filters. E.g. if target_service_name.value is "http" and // target_operating_system.osclass.family is "Linux" then the plugin will only // match if the service is http and the operating system is Linux. TargetOperatingSystemClass *TargetOperatingSystemClass `protobuf:"bytes,5,opt,name=target_operating_system_class,json=targetOperatingSystemClass,proto3" json:"target_operating_system_class,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *PluginDefinition) Reset() { *x = PluginDefinition{} mi := &file_plugin_representation_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *PluginDefinition) String() string { return protoimpl.X.MessageStringOf(x) } func (*PluginDefinition) ProtoMessage() {} func (x *PluginDefinition) ProtoReflect() protoreflect.Message { mi := &file_plugin_representation_proto_msgTypes[0] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use PluginDefinition.ProtoReflect.Descriptor instead. func (*PluginDefinition) Descriptor() ([]byte, []int) { return file_plugin_representation_proto_rawDescGZIP(), []int{0} } func (x *PluginDefinition) GetInfo() *PluginInfo { if x != nil { return x.Info } return nil } func (x *PluginDefinition) GetTargetServiceName() *TargetServiceName { if x != nil { return x.TargetServiceName } return nil } func (x *PluginDefinition) GetTargetSoftware() *TargetSoftware { if x != nil { return x.TargetSoftware } return nil } func (x *PluginDefinition) GetForWebService() bool { if x != nil { return x.ForWebService } return false } func (x *PluginDefinition) GetTargetOperatingSystemClass() *TargetOperatingSystemClass { if x != nil { return x.TargetOperatingSystemClass } return nil } // Represents a PluginInfo annotation placeholder used by the // PluginDefinition proto above. type PluginInfo struct { state protoimpl.MessageState `protogen:"open.v1"` // Type of plugin. Type PluginInfo_PluginType `protobuf:"varint,1,opt,name=type,proto3,enum=tsunami.proto.PluginInfo_PluginType" json:"type,omitempty"` // Name of the plugin. Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` // Version of the plugin Version string `protobuf:"bytes,3,opt,name=version,proto3" json:"version,omitempty"` // Description of the plugin. Description string `protobuf:"bytes,4,opt,name=description,proto3" json:"description,omitempty"` // Author of the plugin. Author string `protobuf:"bytes,5,opt,name=author,proto3" json:"author,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *PluginInfo) Reset() { *x = PluginInfo{} mi := &file_plugin_representation_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *PluginInfo) String() string { return protoimpl.X.MessageStringOf(x) } func (*PluginInfo) ProtoMessage() {} func (x *PluginInfo) ProtoReflect() protoreflect.Message { mi := &file_plugin_representation_proto_msgTypes[1] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use PluginInfo.ProtoReflect.Descriptor instead. func (*PluginInfo) Descriptor() ([]byte, []int) { return file_plugin_representation_proto_rawDescGZIP(), []int{1} } func (x *PluginInfo) GetType() PluginInfo_PluginType { if x != nil { return x.Type } return PluginInfo_PLUGIN_TYPE_UNSPECIFIED } func (x *PluginInfo) GetName() string { if x != nil { return x.Name } return "" } func (x *PluginInfo) GetVersion() string { if x != nil { return x.Version } return "" } func (x *PluginInfo) GetDescription() string { if x != nil { return x.Description } return "" } func (x *PluginInfo) GetAuthor() string { if x != nil { return x.Author } return "" } // Represents a ForServiceName annotation placeholder used by the // PluginDefinition proto above. type TargetServiceName struct { state protoimpl.MessageState `protogen:"open.v1"` // The value of the name of the target. Value []string `protobuf:"bytes,1,rep,name=value,proto3" json:"value,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *TargetServiceName) Reset() { *x = TargetServiceName{} mi := &file_plugin_representation_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *TargetServiceName) String() string { return protoimpl.X.MessageStringOf(x) } func (*TargetServiceName) ProtoMessage() {} func (x *TargetServiceName) ProtoReflect() protoreflect.Message { mi := &file_plugin_representation_proto_msgTypes[2] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use TargetServiceName.ProtoReflect.Descriptor instead. func (*TargetServiceName) Descriptor() ([]byte, []int) { return file_plugin_representation_proto_rawDescGZIP(), []int{2} } func (x *TargetServiceName) GetValue() []string { if x != nil { return x.Value } return nil } // Represents a ForSoftware annotation placeholder used by the // PluginDefinition proto above. type TargetSoftware struct { state protoimpl.MessageState `protogen:"open.v1"` // The name of the target software, case insensitive. Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` // Array of versions and version ranges of the target software. Value []string `protobuf:"bytes,2,rep,name=value,proto3" json:"value,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *TargetSoftware) Reset() { *x = TargetSoftware{} mi := &file_plugin_representation_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *TargetSoftware) String() string { return protoimpl.X.MessageStringOf(x) } func (*TargetSoftware) ProtoMessage() {} func (x *TargetSoftware) ProtoReflect() protoreflect.Message { mi := &file_plugin_representation_proto_msgTypes[3] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use TargetSoftware.ProtoReflect.Descriptor instead. func (*TargetSoftware) Descriptor() ([]byte, []int) { return file_plugin_representation_proto_rawDescGZIP(), []int{3} } func (x *TargetSoftware) GetName() string { if x != nil { return x.Name } return "" } func (x *TargetSoftware) GetValue() []string { if x != nil { return x.Value } return nil } // Represents a ForOperatingSystem annotation placeholder used by the // PluginDefinition proto above. These values are coming directly from the // port scanner's output (e.g. nmap). type TargetOperatingSystemClass struct { state protoimpl.MessageState `protogen:"open.v1"` // The vendor of the target operating system, e.g. "Microsoft" Vendor []string `protobuf:"bytes,1,rep,name=vendor,proto3" json:"vendor,omitempty"` // The family of the target operating system, e.g. "Windows" OsFamily []string `protobuf:"bytes,2,rep,name=os_family,json=osFamily,proto3" json:"os_family,omitempty"` // The minimum accuracy of the target operating system, e.g. 90 MinAccuracy uint32 `protobuf:"varint,3,opt,name=min_accuracy,json=minAccuracy,proto3" json:"min_accuracy,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *TargetOperatingSystemClass) Reset() { *x = TargetOperatingSystemClass{} mi := &file_plugin_representation_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *TargetOperatingSystemClass) String() string { return protoimpl.X.MessageStringOf(x) } func (*TargetOperatingSystemClass) ProtoMessage() {} func (x *TargetOperatingSystemClass) ProtoReflect() protoreflect.Message { mi := &file_plugin_representation_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use TargetOperatingSystemClass.ProtoReflect.Descriptor instead. func (*TargetOperatingSystemClass) Descriptor() ([]byte, []int) { return file_plugin_representation_proto_rawDescGZIP(), []int{4} } func (x *TargetOperatingSystemClass) GetVendor() []string { if x != nil { return x.Vendor } return nil } func (x *TargetOperatingSystemClass) GetOsFamily() []string { if x != nil { return x.OsFamily } return nil } func (x *TargetOperatingSystemClass) GetMinAccuracy() uint32 { if x != nil { return x.MinAccuracy } return 0 } var File_plugin_representation_proto protoreflect.FileDescriptor var file_plugin_representation_proto_rawDesc = string([]byte{ 0x0a, 0x1b, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x5f, 0x72, 0x65, 0x70, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0d, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xf1, 0x02, 0x0a, 0x10, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x44, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2d, 0x0a, 0x04, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x04, 0x69, 0x6e, 0x66, 0x6f, 0x12, 0x50, 0x0a, 0x13, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x52, 0x11, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x46, 0x0a, 0x0f, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x73, 0x6f, 0x66, 0x74, 0x77, 0x61, 0x72, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x53, 0x6f, 0x66, 0x74, 0x77, 0x61, 0x72, 0x65, 0x52, 0x0e, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x53, 0x6f, 0x66, 0x74, 0x77, 0x61, 0x72, 0x65, 0x12, 0x26, 0x0a, 0x0f, 0x66, 0x6f, 0x72, 0x5f, 0x77, 0x65, 0x62, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x66, 0x6f, 0x72, 0x57, 0x65, 0x62, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x6c, 0x0a, 0x1d, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6e, 0x67, 0x5f, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x5f, 0x63, 0x6c, 0x61, 0x73, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6e, 0x67, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x52, 0x1a, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6e, 0x67, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x22, 0x95, 0x02, 0x0a, 0x0a, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x38, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x24, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x49, 0x6e, 0x66, 0x6f, 0x2e, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x22, 0x65, 0x0a, 0x0a, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1b, 0x0a, 0x17, 0x50, 0x4c, 0x55, 0x47, 0x49, 0x4e, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0d, 0x0a, 0x09, 0x50, 0x4f, 0x52, 0x54, 0x5f, 0x53, 0x43, 0x41, 0x4e, 0x10, 0x01, 0x12, 0x17, 0x0a, 0x13, 0x53, 0x45, 0x52, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x46, 0x49, 0x4e, 0x47, 0x45, 0x52, 0x50, 0x52, 0x49, 0x4e, 0x54, 0x10, 0x02, 0x12, 0x12, 0x0a, 0x0e, 0x56, 0x55, 0x4c, 0x4e, 0x5f, 0x44, 0x45, 0x54, 0x45, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x10, 0x03, 0x22, 0x29, 0x0a, 0x11, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x3a, 0x0a, 0x0e, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x53, 0x6f, 0x66, 0x74, 0x77, 0x61, 0x72, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x74, 0x0a, 0x1a, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6e, 0x67, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x76, 0x65, 0x6e, 0x64, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x76, 0x65, 0x6e, 0x64, 0x6f, 0x72, 0x12, 0x1b, 0x0a, 0x09, 0x6f, 0x73, 0x5f, 0x66, 0x61, 0x6d, 0x69, 0x6c, 0x79, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x6f, 0x73, 0x46, 0x61, 0x6d, 0x69, 0x6c, 0x79, 0x12, 0x21, 0x0a, 0x0c, 0x6d, 0x69, 0x6e, 0x5f, 0x61, 0x63, 0x63, 0x75, 0x72, 0x61, 0x63, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0b, 0x6d, 0x69, 0x6e, 0x41, 0x63, 0x63, 0x75, 0x72, 0x61, 0x63, 0x79, 0x42, 0x7b, 0x0a, 0x18, 0x63, 0x6f, 0x6d, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x42, 0x1a, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x70, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x50, 0x01, 0x5a, 0x41, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2d, 0x73, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x2d, 0x73, 0x63, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x5f, 0x67, 0x6f, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, }) var ( file_plugin_representation_proto_rawDescOnce sync.Once file_plugin_representation_proto_rawDescData []byte ) func file_plugin_representation_proto_rawDescGZIP() []byte { file_plugin_representation_proto_rawDescOnce.Do(func() { file_plugin_representation_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_plugin_representation_proto_rawDesc), len(file_plugin_representation_proto_rawDesc))) }) return file_plugin_representation_proto_rawDescData } var file_plugin_representation_proto_enumTypes = make([]protoimpl.EnumInfo, 1) var file_plugin_representation_proto_msgTypes = make([]protoimpl.MessageInfo, 5) var file_plugin_representation_proto_goTypes = []any{ (PluginInfo_PluginType)(0), // 0: tsunami.proto.PluginInfo.PluginType (*PluginDefinition)(nil), // 1: tsunami.proto.PluginDefinition (*PluginInfo)(nil), // 2: tsunami.proto.PluginInfo (*TargetServiceName)(nil), // 3: tsunami.proto.TargetServiceName (*TargetSoftware)(nil), // 4: tsunami.proto.TargetSoftware (*TargetOperatingSystemClass)(nil), // 5: tsunami.proto.TargetOperatingSystemClass } var file_plugin_representation_proto_depIdxs = []int32{ 2, // 0: tsunami.proto.PluginDefinition.info:type_name -> tsunami.proto.PluginInfo 3, // 1: tsunami.proto.PluginDefinition.target_service_name:type_name -> tsunami.proto.TargetServiceName 4, // 2: tsunami.proto.PluginDefinition.target_software:type_name -> tsunami.proto.TargetSoftware 5, // 3: tsunami.proto.PluginDefinition.target_operating_system_class:type_name -> tsunami.proto.TargetOperatingSystemClass 0, // 4: tsunami.proto.PluginInfo.type:type_name -> tsunami.proto.PluginInfo.PluginType 5, // [5:5] is the sub-list for method output_type 5, // [5:5] is the sub-list for method input_type 5, // [5:5] is the sub-list for extension type_name 5, // [5:5] is the sub-list for extension extendee 0, // [0:5] is the sub-list for field type_name } func init() { file_plugin_representation_proto_init() } func file_plugin_representation_proto_init() { if File_plugin_representation_proto != nil { return } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_plugin_representation_proto_rawDesc), len(file_plugin_representation_proto_rawDesc)), NumEnums: 1, NumMessages: 5, NumExtensions: 0, NumServices: 0, }, GoTypes: file_plugin_representation_proto_goTypes, DependencyIndexes: file_plugin_representation_proto_depIdxs, EnumInfos: file_plugin_representation_proto_enumTypes, MessageInfos: file_plugin_representation_proto_msgTypes, }.Build() File_plugin_representation_proto = out.File file_plugin_representation_proto_goTypes = nil file_plugin_representation_proto_depIdxs = nil } ================================================ FILE: proto/tsunami_go_proto/plugin_service.pb.go ================================================ // // Copyright 2022 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Model for the plugin RPC service protocol. // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.5 // protoc v3.21.12 // source: plugin_service.proto package tsunami_go_proto import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" unsafe "unsafe" ) const ( // Verify that this generated code is sufficiently up-to-date. _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) // Verify that runtime/protoimpl is sufficiently up-to-date. _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) // Represents a run request with all matched plugins that will need to run // as well as the target to run against. type RunRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // Target of the plugins. Target *TargetInfo `protobuf:"bytes,1,opt,name=target,proto3" json:"target,omitempty"` // All matched plugins that will need to run. Plugins []*MatchedPlugin `protobuf:"bytes,2,rep,name=plugins,proto3" json:"plugins,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *RunRequest) Reset() { *x = RunRequest{} mi := &file_plugin_service_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *RunRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*RunRequest) ProtoMessage() {} func (x *RunRequest) ProtoReflect() protoreflect.Message { mi := &file_plugin_service_proto_msgTypes[0] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use RunRequest.ProtoReflect.Descriptor instead. func (*RunRequest) Descriptor() ([]byte, []int) { return file_plugin_service_proto_rawDescGZIP(), []int{0} } func (x *RunRequest) GetTarget() *TargetInfo { if x != nil { return x.Target } return nil } func (x *RunRequest) GetPlugins() []*MatchedPlugin { if x != nil { return x.Plugins } return nil } // Compact representation of RunRequest. type RunCompactRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // Target of the plugins. Target *TargetInfo `protobuf:"bytes,1,opt,name=target,proto3" json:"target,omitempty"` // All network services that are targeted by some of the plugins. Services []*NetworkService `protobuf:"bytes,2,rep,name=services,proto3" json:"services,omitempty"` // All plugins that should be executed during the run. Plugins []*PluginDefinition `protobuf:"bytes,3,rep,name=plugins,proto3" json:"plugins,omitempty"` // The concrete map of plugin/network service pairs that should be scanned. ScanTargets []*RunCompactRequest_PluginNetworkServiceTarget `protobuf:"bytes,4,rep,name=scan_targets,json=scanTargets,proto3" json:"scan_targets,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *RunCompactRequest) Reset() { *x = RunCompactRequest{} mi := &file_plugin_service_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *RunCompactRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*RunCompactRequest) ProtoMessage() {} func (x *RunCompactRequest) ProtoReflect() protoreflect.Message { mi := &file_plugin_service_proto_msgTypes[1] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use RunCompactRequest.ProtoReflect.Descriptor instead. func (*RunCompactRequest) Descriptor() ([]byte, []int) { return file_plugin_service_proto_rawDescGZIP(), []int{1} } func (x *RunCompactRequest) GetTarget() *TargetInfo { if x != nil { return x.Target } return nil } func (x *RunCompactRequest) GetServices() []*NetworkService { if x != nil { return x.Services } return nil } func (x *RunCompactRequest) GetPlugins() []*PluginDefinition { if x != nil { return x.Plugins } return nil } func (x *RunCompactRequest) GetScanTargets() []*RunCompactRequest_PluginNetworkServiceTarget { if x != nil { return x.ScanTargets } return nil } // Represents the plugin needed to run by the language-specific server // as well as all the matched network services for the plugin. type MatchedPlugin struct { state protoimpl.MessageState `protogen:"open.v1"` // All matched network services from the reconnaissance report. Services []*NetworkService `protobuf:"bytes,1,rep,name=services,proto3" json:"services,omitempty"` // Plugin to run. Plugin *PluginDefinition `protobuf:"bytes,2,opt,name=plugin,proto3" json:"plugin,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *MatchedPlugin) Reset() { *x = MatchedPlugin{} mi := &file_plugin_service_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *MatchedPlugin) String() string { return protoimpl.X.MessageStringOf(x) } func (*MatchedPlugin) ProtoMessage() {} func (x *MatchedPlugin) ProtoReflect() protoreflect.Message { mi := &file_plugin_service_proto_msgTypes[2] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use MatchedPlugin.ProtoReflect.Descriptor instead. func (*MatchedPlugin) Descriptor() ([]byte, []int) { return file_plugin_service_proto_rawDescGZIP(), []int{2} } func (x *MatchedPlugin) GetServices() []*NetworkService { if x != nil { return x.Services } return nil } func (x *MatchedPlugin) GetPlugin() *PluginDefinition { if x != nil { return x.Plugin } return nil } // Represents a run response with the only field being all DetectionReports // generated by the language-specific server. type RunResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Reports *DetectionReportList `protobuf:"bytes,1,opt,name=reports,proto3" json:"reports,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *RunResponse) Reset() { *x = RunResponse{} mi := &file_plugin_service_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *RunResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*RunResponse) ProtoMessage() {} func (x *RunResponse) ProtoReflect() protoreflect.Message { mi := &file_plugin_service_proto_msgTypes[3] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use RunResponse.ProtoReflect.Descriptor instead. func (*RunResponse) Descriptor() ([]byte, []int) { return file_plugin_service_proto_rawDescGZIP(), []int{3} } func (x *RunResponse) GetReports() *DetectionReportList { if x != nil { return x.Reports } return nil } // Represents a request to list all plugins from the requested server. type ListPluginsRequest struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *ListPluginsRequest) Reset() { *x = ListPluginsRequest{} mi := &file_plugin_service_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *ListPluginsRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*ListPluginsRequest) ProtoMessage() {} func (x *ListPluginsRequest) ProtoReflect() protoreflect.Message { mi := &file_plugin_service_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use ListPluginsRequest.ProtoReflect.Descriptor instead. func (*ListPluginsRequest) Descriptor() ([]byte, []int) { return file_plugin_service_proto_rawDescGZIP(), []int{4} } // Represents a response containing a list of all plugins // from the requested server. type ListPluginsResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Plugins []*PluginDefinition `protobuf:"bytes,1,rep,name=plugins,proto3" json:"plugins,omitempty"` // Plugin service can indicate here that it RunRequest should be compact // (compact_targets should be populated instead of MatchedPlugin plugins). WantCompactRunRequest bool `protobuf:"varint,2,opt,name=want_compact_run_request,json=wantCompactRunRequest,proto3" json:"want_compact_run_request,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *ListPluginsResponse) Reset() { *x = ListPluginsResponse{} mi := &file_plugin_service_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *ListPluginsResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*ListPluginsResponse) ProtoMessage() {} func (x *ListPluginsResponse) ProtoReflect() protoreflect.Message { mi := &file_plugin_service_proto_msgTypes[5] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use ListPluginsResponse.ProtoReflect.Descriptor instead. func (*ListPluginsResponse) Descriptor() ([]byte, []int) { return file_plugin_service_proto_rawDescGZIP(), []int{5} } func (x *ListPluginsResponse) GetPlugins() []*PluginDefinition { if x != nil { return x.Plugins } return nil } func (x *ListPluginsResponse) GetWantCompactRunRequest() bool { if x != nil { return x.WantCompactRunRequest } return false } // Indexes in the following structure point to the services/plugins defined // below. (The order is safe, guaranteed by the proto specification: "The // order of the elements with respect to each other is preserved when parsing, // though the ordering with respect to other fields is lost.") type RunCompactRequest_PluginNetworkServiceTarget struct { state protoimpl.MessageState `protogen:"open.v1"` // The index of the plugin to run. PluginIndex uint32 `protobuf:"varint,1,opt,name=plugin_index,json=pluginIndex,proto3" json:"plugin_index,omitempty"` // The index of the network service to run against. ServiceIndex uint32 `protobuf:"varint,2,opt,name=service_index,json=serviceIndex,proto3" json:"service_index,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *RunCompactRequest_PluginNetworkServiceTarget) Reset() { *x = RunCompactRequest_PluginNetworkServiceTarget{} mi := &file_plugin_service_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *RunCompactRequest_PluginNetworkServiceTarget) String() string { return protoimpl.X.MessageStringOf(x) } func (*RunCompactRequest_PluginNetworkServiceTarget) ProtoMessage() {} func (x *RunCompactRequest_PluginNetworkServiceTarget) ProtoReflect() protoreflect.Message { mi := &file_plugin_service_proto_msgTypes[6] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use RunCompactRequest_PluginNetworkServiceTarget.ProtoReflect.Descriptor instead. func (*RunCompactRequest_PluginNetworkServiceTarget) Descriptor() ([]byte, []int) { return file_plugin_service_proto_rawDescGZIP(), []int{1, 0} } func (x *RunCompactRequest_PluginNetworkServiceTarget) GetPluginIndex() uint32 { if x != nil { return x.PluginIndex } return 0 } func (x *RunCompactRequest_PluginNetworkServiceTarget) GetServiceIndex() uint32 { if x != nil { return x.ServiceIndex } return 0 } var File_plugin_service_proto protoreflect.FileDescriptor var file_plugin_service_proto_rawDesc = string([]byte{ 0x0a, 0x14, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0d, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x0f, 0x64, 0x65, 0x74, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x15, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1b, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x5f, 0x72, 0x65, 0x70, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x14, 0x72, 0x65, 0x63, 0x6f, 0x6e, 0x6e, 0x61, 0x69, 0x73, 0x73, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x77, 0x0a, 0x0a, 0x52, 0x75, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x31, 0x0a, 0x06, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x06, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x12, 0x36, 0x0a, 0x07, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x64, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x52, 0x07, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x73, 0x22, 0x82, 0x03, 0x0a, 0x11, 0x52, 0x75, 0x6e, 0x43, 0x6f, 0x6d, 0x70, 0x61, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x31, 0x0a, 0x06, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x06, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x12, 0x39, 0x0a, 0x08, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x08, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x12, 0x39, 0x0a, 0x07, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x44, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x07, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x73, 0x12, 0x5e, 0x0a, 0x0c, 0x73, 0x63, 0x61, 0x6e, 0x5f, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3b, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x75, 0x6e, 0x43, 0x6f, 0x6d, 0x70, 0x61, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x52, 0x0b, 0x73, 0x63, 0x61, 0x6e, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x73, 0x1a, 0x64, 0x0a, 0x1a, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x5f, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0b, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x23, 0x0a, 0x0d, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0c, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x22, 0x83, 0x01, 0x0a, 0x0d, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x64, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x12, 0x39, 0x0a, 0x08, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x08, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x12, 0x37, 0x0a, 0x06, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x44, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x22, 0x4b, 0x0a, 0x0b, 0x52, 0x75, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3c, 0x0a, 0x07, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x44, 0x65, 0x74, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x07, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x73, 0x22, 0x14, 0x0a, 0x12, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x89, 0x01, 0x0a, 0x13, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x39, 0x0a, 0x07, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x44, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x07, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x73, 0x12, 0x37, 0x0a, 0x18, 0x77, 0x61, 0x6e, 0x74, 0x5f, 0x63, 0x6f, 0x6d, 0x70, 0x61, 0x63, 0x74, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x15, 0x77, 0x61, 0x6e, 0x74, 0x43, 0x6f, 0x6d, 0x70, 0x61, 0x63, 0x74, 0x52, 0x75, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x32, 0xf5, 0x01, 0x0a, 0x0d, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x3e, 0x0a, 0x03, 0x52, 0x75, 0x6e, 0x12, 0x19, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x75, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x75, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x4c, 0x0a, 0x0a, 0x52, 0x75, 0x6e, 0x43, 0x6f, 0x6d, 0x70, 0x61, 0x63, 0x74, 0x12, 0x20, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x75, 0x6e, 0x43, 0x6f, 0x6d, 0x70, 0x61, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x75, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x56, 0x0a, 0x0b, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x73, 0x12, 0x21, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x74, 0x0a, 0x18, 0x63, 0x6f, 0x6d, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x42, 0x13, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x50, 0x01, 0x5a, 0x41, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2d, 0x73, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x2d, 0x73, 0x63, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x5f, 0x67, 0x6f, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, }) var ( file_plugin_service_proto_rawDescOnce sync.Once file_plugin_service_proto_rawDescData []byte ) func file_plugin_service_proto_rawDescGZIP() []byte { file_plugin_service_proto_rawDescOnce.Do(func() { file_plugin_service_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_plugin_service_proto_rawDesc), len(file_plugin_service_proto_rawDesc))) }) return file_plugin_service_proto_rawDescData } var file_plugin_service_proto_msgTypes = make([]protoimpl.MessageInfo, 7) var file_plugin_service_proto_goTypes = []any{ (*RunRequest)(nil), // 0: tsunami.proto.RunRequest (*RunCompactRequest)(nil), // 1: tsunami.proto.RunCompactRequest (*MatchedPlugin)(nil), // 2: tsunami.proto.MatchedPlugin (*RunResponse)(nil), // 3: tsunami.proto.RunResponse (*ListPluginsRequest)(nil), // 4: tsunami.proto.ListPluginsRequest (*ListPluginsResponse)(nil), // 5: tsunami.proto.ListPluginsResponse (*RunCompactRequest_PluginNetworkServiceTarget)(nil), // 6: tsunami.proto.RunCompactRequest.PluginNetworkServiceTarget (*TargetInfo)(nil), // 7: tsunami.proto.TargetInfo (*NetworkService)(nil), // 8: tsunami.proto.NetworkService (*PluginDefinition)(nil), // 9: tsunami.proto.PluginDefinition (*DetectionReportList)(nil), // 10: tsunami.proto.DetectionReportList } var file_plugin_service_proto_depIdxs = []int32{ 7, // 0: tsunami.proto.RunRequest.target:type_name -> tsunami.proto.TargetInfo 2, // 1: tsunami.proto.RunRequest.plugins:type_name -> tsunami.proto.MatchedPlugin 7, // 2: tsunami.proto.RunCompactRequest.target:type_name -> tsunami.proto.TargetInfo 8, // 3: tsunami.proto.RunCompactRequest.services:type_name -> tsunami.proto.NetworkService 9, // 4: tsunami.proto.RunCompactRequest.plugins:type_name -> tsunami.proto.PluginDefinition 6, // 5: tsunami.proto.RunCompactRequest.scan_targets:type_name -> tsunami.proto.RunCompactRequest.PluginNetworkServiceTarget 8, // 6: tsunami.proto.MatchedPlugin.services:type_name -> tsunami.proto.NetworkService 9, // 7: tsunami.proto.MatchedPlugin.plugin:type_name -> tsunami.proto.PluginDefinition 10, // 8: tsunami.proto.RunResponse.reports:type_name -> tsunami.proto.DetectionReportList 9, // 9: tsunami.proto.ListPluginsResponse.plugins:type_name -> tsunami.proto.PluginDefinition 0, // 10: tsunami.proto.PluginService.Run:input_type -> tsunami.proto.RunRequest 1, // 11: tsunami.proto.PluginService.RunCompact:input_type -> tsunami.proto.RunCompactRequest 4, // 12: tsunami.proto.PluginService.ListPlugins:input_type -> tsunami.proto.ListPluginsRequest 3, // 13: tsunami.proto.PluginService.Run:output_type -> tsunami.proto.RunResponse 3, // 14: tsunami.proto.PluginService.RunCompact:output_type -> tsunami.proto.RunResponse 5, // 15: tsunami.proto.PluginService.ListPlugins:output_type -> tsunami.proto.ListPluginsResponse 13, // [13:16] is the sub-list for method output_type 10, // [10:13] is the sub-list for method input_type 10, // [10:10] is the sub-list for extension type_name 10, // [10:10] is the sub-list for extension extendee 0, // [0:10] is the sub-list for field type_name } func init() { file_plugin_service_proto_init() } func file_plugin_service_proto_init() { if File_plugin_service_proto != nil { return } file_detection_proto_init() file_network_service_proto_init() file_plugin_representation_proto_init() file_reconnaissance_proto_init() type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_plugin_service_proto_rawDesc), len(file_plugin_service_proto_rawDesc)), NumEnums: 0, NumMessages: 7, NumExtensions: 0, NumServices: 1, }, GoTypes: file_plugin_service_proto_goTypes, DependencyIndexes: file_plugin_service_proto_depIdxs, MessageInfos: file_plugin_service_proto_msgTypes, }.Build() File_plugin_service_proto = out.File file_plugin_service_proto_goTypes = nil file_plugin_service_proto_depIdxs = nil } ================================================ FILE: proto/tsunami_go_proto/reconnaissance.pb.go ================================================ // // Copyright 2019 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Data models for all the reconnaissance information gathered by Tsunami. // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.5 // protoc v3.21.12 // source: reconnaissance.proto package tsunami_go_proto import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" unsafe "unsafe" ) const ( // Verify that this generated code is sufficiently up-to-date. _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) // Verify that runtime/protoimpl is sufficiently up-to-date. _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) // Detailed information about the scanning target. type TargetInfo struct { state protoimpl.MessageState `protogen:"open.v1"` // All the known network endpoints of the scanning target. NetworkEndpoints []*NetworkEndpoint `protobuf:"bytes,1,rep,name=network_endpoints,json=networkEndpoints,proto3" json:"network_endpoints,omitempty"` OperatingSystemClasses []*OperatingSystemClass `protobuf:"bytes,2,rep,name=operating_system_classes,json=operatingSystemClasses,proto3" json:"operating_system_classes,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *TargetInfo) Reset() { *x = TargetInfo{} mi := &file_reconnaissance_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *TargetInfo) String() string { return protoimpl.X.MessageStringOf(x) } func (*TargetInfo) ProtoMessage() {} func (x *TargetInfo) ProtoReflect() protoreflect.Message { mi := &file_reconnaissance_proto_msgTypes[0] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use TargetInfo.ProtoReflect.Descriptor instead. func (*TargetInfo) Descriptor() ([]byte, []int) { return file_reconnaissance_proto_rawDescGZIP(), []int{0} } func (x *TargetInfo) GetNetworkEndpoints() []*NetworkEndpoint { if x != nil { return x.NetworkEndpoints } return nil } func (x *TargetInfo) GetOperatingSystemClasses() []*OperatingSystemClass { if x != nil { return x.OperatingSystemClasses } return nil } // Represents a ForOperatingSystem annotation placeholder used by the // PluginDefinition proto above. // For possible values, consult the following database: // https://raw.githubusercontent.com/nmap/nmap/master/nmap-os-db type OperatingSystemClass struct { state protoimpl.MessageState `protogen:"open.v1"` // The type of the target operating system, e.g. "general purpose" Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` // The vendor of the target operating system, e.g. "Linux" Vendor string `protobuf:"bytes,2,opt,name=vendor,proto3" json:"vendor,omitempty"` // The family of the target operating system, e.g. "Linux" OsFamily string `protobuf:"bytes,3,opt,name=os_family,json=osFamily,proto3" json:"os_family,omitempty"` // The generation of the target operating system, e.g. "2.6.X" OsGeneration string `protobuf:"bytes,4,opt,name=os_generation,json=osGeneration,proto3" json:"os_generation,omitempty"` // The estimated accuracy of the target operating system, e.g. 90 Accuracy uint32 `protobuf:"varint,5,opt,name=accuracy,proto3" json:"accuracy,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *OperatingSystemClass) Reset() { *x = OperatingSystemClass{} mi := &file_reconnaissance_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *OperatingSystemClass) String() string { return protoimpl.X.MessageStringOf(x) } func (*OperatingSystemClass) ProtoMessage() {} func (x *OperatingSystemClass) ProtoReflect() protoreflect.Message { mi := &file_reconnaissance_proto_msgTypes[1] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use OperatingSystemClass.ProtoReflect.Descriptor instead. func (*OperatingSystemClass) Descriptor() ([]byte, []int) { return file_reconnaissance_proto_rawDescGZIP(), []int{1} } func (x *OperatingSystemClass) GetType() string { if x != nil { return x.Type } return "" } func (x *OperatingSystemClass) GetVendor() string { if x != nil { return x.Vendor } return "" } func (x *OperatingSystemClass) GetOsFamily() string { if x != nil { return x.OsFamily } return "" } func (x *OperatingSystemClass) GetOsGeneration() string { if x != nil { return x.OsGeneration } return "" } func (x *OperatingSystemClass) GetAccuracy() uint32 { if x != nil { return x.Accuracy } return 0 } // Report from a port scanner. type PortScanningReport struct { state protoimpl.MessageState `protogen:"open.v1"` // Information about the scanning target. TargetInfo *TargetInfo `protobuf:"bytes,1,opt,name=target_info,json=targetInfo,proto3" json:"target_info,omitempty"` // List of all the exposed network services. NetworkServices []*NetworkService `protobuf:"bytes,2,rep,name=network_services,json=networkServices,proto3" json:"network_services,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *PortScanningReport) Reset() { *x = PortScanningReport{} mi := &file_reconnaissance_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *PortScanningReport) String() string { return protoimpl.X.MessageStringOf(x) } func (*PortScanningReport) ProtoMessage() {} func (x *PortScanningReport) ProtoReflect() protoreflect.Message { mi := &file_reconnaissance_proto_msgTypes[2] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use PortScanningReport.ProtoReflect.Descriptor instead. func (*PortScanningReport) Descriptor() ([]byte, []int) { return file_reconnaissance_proto_rawDescGZIP(), []int{2} } func (x *PortScanningReport) GetTargetInfo() *TargetInfo { if x != nil { return x.TargetInfo } return nil } func (x *PortScanningReport) GetNetworkServices() []*NetworkService { if x != nil { return x.NetworkServices } return nil } // Report from a service fingerprinter. type FingerprintingReport struct { state protoimpl.MessageState `protogen:"open.v1"` // List of all the identified network services after fingerprinting. NetworkServices []*NetworkService `protobuf:"bytes,3,rep,name=network_services,json=networkServices,proto3" json:"network_services,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *FingerprintingReport) Reset() { *x = FingerprintingReport{} mi := &file_reconnaissance_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *FingerprintingReport) String() string { return protoimpl.X.MessageStringOf(x) } func (*FingerprintingReport) ProtoMessage() {} func (x *FingerprintingReport) ProtoReflect() protoreflect.Message { mi := &file_reconnaissance_proto_msgTypes[3] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use FingerprintingReport.ProtoReflect.Descriptor instead. func (*FingerprintingReport) Descriptor() ([]byte, []int) { return file_reconnaissance_proto_rawDescGZIP(), []int{3} } func (x *FingerprintingReport) GetNetworkServices() []*NetworkService { if x != nil { return x.NetworkServices } return nil } // Full reconnaissance report about a single scanning target. type ReconnaissanceReport struct { state protoimpl.MessageState `protogen:"open.v1"` // Information about the scanning target. TargetInfo *TargetInfo `protobuf:"bytes,1,opt,name=target_info,json=targetInfo,proto3" json:"target_info,omitempty"` // All exposed network services of the scanning target. NetworkServices []*NetworkService `protobuf:"bytes,2,rep,name=network_services,json=networkServices,proto3" json:"network_services,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *ReconnaissanceReport) Reset() { *x = ReconnaissanceReport{} mi := &file_reconnaissance_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *ReconnaissanceReport) String() string { return protoimpl.X.MessageStringOf(x) } func (*ReconnaissanceReport) ProtoMessage() {} func (x *ReconnaissanceReport) ProtoReflect() protoreflect.Message { mi := &file_reconnaissance_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use ReconnaissanceReport.ProtoReflect.Descriptor instead. func (*ReconnaissanceReport) Descriptor() ([]byte, []int) { return file_reconnaissance_proto_rawDescGZIP(), []int{4} } func (x *ReconnaissanceReport) GetTargetInfo() *TargetInfo { if x != nil { return x.TargetInfo } return nil } func (x *ReconnaissanceReport) GetNetworkServices() []*NetworkService { if x != nil { return x.NetworkServices } return nil } var File_reconnaissance_proto protoreflect.FileDescriptor var file_reconnaissance_proto_rawDesc = string([]byte{ 0x0a, 0x14, 0x72, 0x65, 0x63, 0x6f, 0x6e, 0x6e, 0x61, 0x69, 0x73, 0x73, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0d, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x0d, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x15, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xb8, 0x01, 0x0a, 0x0a, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x4b, 0x0a, 0x11, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x5f, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x52, 0x10, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x12, 0x5d, 0x0a, 0x18, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6e, 0x67, 0x5f, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x5f, 0x63, 0x6c, 0x61, 0x73, 0x73, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6e, 0x67, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x52, 0x16, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6e, 0x67, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x65, 0x73, 0x22, 0xa0, 0x01, 0x0a, 0x14, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6e, 0x67, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x76, 0x65, 0x6e, 0x64, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x76, 0x65, 0x6e, 0x64, 0x6f, 0x72, 0x12, 0x1b, 0x0a, 0x09, 0x6f, 0x73, 0x5f, 0x66, 0x61, 0x6d, 0x69, 0x6c, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6f, 0x73, 0x46, 0x61, 0x6d, 0x69, 0x6c, 0x79, 0x12, 0x23, 0x0a, 0x0d, 0x6f, 0x73, 0x5f, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x6f, 0x73, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x61, 0x63, 0x63, 0x75, 0x72, 0x61, 0x63, 0x79, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x08, 0x61, 0x63, 0x63, 0x75, 0x72, 0x61, 0x63, 0x79, 0x22, 0x9a, 0x01, 0x0a, 0x12, 0x50, 0x6f, 0x72, 0x74, 0x53, 0x63, 0x61, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x3a, 0x0a, 0x0b, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0a, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x48, 0x0a, 0x10, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x0f, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x22, 0x60, 0x0a, 0x14, 0x46, 0x69, 0x6e, 0x67, 0x65, 0x72, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x48, 0x0a, 0x10, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x0f, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x22, 0x9c, 0x01, 0x0a, 0x14, 0x52, 0x65, 0x63, 0x6f, 0x6e, 0x6e, 0x61, 0x69, 0x73, 0x73, 0x61, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x3a, 0x0a, 0x0b, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0a, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x48, 0x0a, 0x10, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x0f, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x42, 0x75, 0x0a, 0x18, 0x63, 0x6f, 0x6d, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x42, 0x14, 0x52, 0x65, 0x63, 0x6f, 0x6e, 0x6e, 0x61, 0x69, 0x73, 0x73, 0x61, 0x6e, 0x63, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x50, 0x01, 0x5a, 0x41, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2d, 0x73, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x2d, 0x73, 0x63, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x5f, 0x67, 0x6f, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, }) var ( file_reconnaissance_proto_rawDescOnce sync.Once file_reconnaissance_proto_rawDescData []byte ) func file_reconnaissance_proto_rawDescGZIP() []byte { file_reconnaissance_proto_rawDescOnce.Do(func() { file_reconnaissance_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_reconnaissance_proto_rawDesc), len(file_reconnaissance_proto_rawDesc))) }) return file_reconnaissance_proto_rawDescData } var file_reconnaissance_proto_msgTypes = make([]protoimpl.MessageInfo, 5) var file_reconnaissance_proto_goTypes = []any{ (*TargetInfo)(nil), // 0: tsunami.proto.TargetInfo (*OperatingSystemClass)(nil), // 1: tsunami.proto.OperatingSystemClass (*PortScanningReport)(nil), // 2: tsunami.proto.PortScanningReport (*FingerprintingReport)(nil), // 3: tsunami.proto.FingerprintingReport (*ReconnaissanceReport)(nil), // 4: tsunami.proto.ReconnaissanceReport (*NetworkEndpoint)(nil), // 5: tsunami.proto.NetworkEndpoint (*NetworkService)(nil), // 6: tsunami.proto.NetworkService } var file_reconnaissance_proto_depIdxs = []int32{ 5, // 0: tsunami.proto.TargetInfo.network_endpoints:type_name -> tsunami.proto.NetworkEndpoint 1, // 1: tsunami.proto.TargetInfo.operating_system_classes:type_name -> tsunami.proto.OperatingSystemClass 0, // 2: tsunami.proto.PortScanningReport.target_info:type_name -> tsunami.proto.TargetInfo 6, // 3: tsunami.proto.PortScanningReport.network_services:type_name -> tsunami.proto.NetworkService 6, // 4: tsunami.proto.FingerprintingReport.network_services:type_name -> tsunami.proto.NetworkService 0, // 5: tsunami.proto.ReconnaissanceReport.target_info:type_name -> tsunami.proto.TargetInfo 6, // 6: tsunami.proto.ReconnaissanceReport.network_services:type_name -> tsunami.proto.NetworkService 7, // [7:7] is the sub-list for method output_type 7, // [7:7] is the sub-list for method input_type 7, // [7:7] is the sub-list for extension type_name 7, // [7:7] is the sub-list for extension extendee 0, // [0:7] is the sub-list for field type_name } func init() { file_reconnaissance_proto_init() } func file_reconnaissance_proto_init() { if File_reconnaissance_proto != nil { return } file_network_proto_init() file_network_service_proto_init() type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_reconnaissance_proto_rawDesc), len(file_reconnaissance_proto_rawDesc)), NumEnums: 0, NumMessages: 5, NumExtensions: 0, NumServices: 0, }, GoTypes: file_reconnaissance_proto_goTypes, DependencyIndexes: file_reconnaissance_proto_depIdxs, MessageInfos: file_reconnaissance_proto_msgTypes, }.Build() File_reconnaissance_proto = out.File file_reconnaissance_proto_goTypes = nil file_reconnaissance_proto_depIdxs = nil } ================================================ FILE: proto/tsunami_go_proto/scan_results.pb.go ================================================ // // Copyright 2020 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Data models for describing scanning results. // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.5 // protoc v3.21.12 // source: scan_results.proto package tsunami_go_proto import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" durationpb "google.golang.org/protobuf/types/known/durationpb" timestamppb "google.golang.org/protobuf/types/known/timestamppb" reflect "reflect" sync "sync" unsafe "unsafe" ) const ( // Verify that this generated code is sufficiently up-to-date. _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) // Verify that runtime/protoimpl is sufficiently up-to-date. _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) // Execution status of the scan. // NEXT ID: 5 type ScanStatus int32 const ( // Unspecified status. ScanStatus_SCAN_STATUS_UNSPECIFIED ScanStatus = 0 // Scan finished successfully. ScanStatus_SUCCEEDED ScanStatus = 1 // Scan finished with only a small set of selected detectors succeeded. ScanStatus_PARTIALLY_SUCCEEDED ScanStatus = 4 // Scan failed. ScanStatus_FAILED ScanStatus = 2 // Scan cancelled. ScanStatus_CANCELLED ScanStatus = 3 ) // Enum value maps for ScanStatus. var ( ScanStatus_name = map[int32]string{ 0: "SCAN_STATUS_UNSPECIFIED", 1: "SUCCEEDED", 4: "PARTIALLY_SUCCEEDED", 2: "FAILED", 3: "CANCELLED", } ScanStatus_value = map[string]int32{ "SCAN_STATUS_UNSPECIFIED": 0, "SUCCEEDED": 1, "PARTIALLY_SUCCEEDED": 4, "FAILED": 2, "CANCELLED": 3, } ) func (x ScanStatus) Enum() *ScanStatus { p := new(ScanStatus) *p = x return p } func (x ScanStatus) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (ScanStatus) Descriptor() protoreflect.EnumDescriptor { return file_scan_results_proto_enumTypes[0].Descriptor() } func (ScanStatus) Type() protoreflect.EnumType { return &file_scan_results_proto_enumTypes[0] } func (x ScanStatus) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } // Deprecated: Use ScanStatus.Descriptor instead. func (ScanStatus) EnumDescriptor() ([]byte, []int) { return file_scan_results_proto_rawDescGZIP(), []int{0} } // A single vulnerability finding for a specific service. type ScanFinding struct { state protoimpl.MessageState `protogen:"open.v1"` // Information about the scanned target. TargetInfo *TargetInfo `protobuf:"bytes,1,opt,name=target_info,json=targetInfo,proto3" json:"target_info,omitempty"` // Information about the scanned network service. NetworkService *NetworkService `protobuf:"bytes,2,opt,name=network_service,json=networkService,proto3" json:"network_service,omitempty"` // Details about the detected vulnerability. Vulnerability *Vulnerability `protobuf:"bytes,3,opt,name=vulnerability,proto3" json:"vulnerability,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *ScanFinding) Reset() { *x = ScanFinding{} mi := &file_scan_results_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *ScanFinding) String() string { return protoimpl.X.MessageStringOf(x) } func (*ScanFinding) ProtoMessage() {} func (x *ScanFinding) ProtoReflect() protoreflect.Message { mi := &file_scan_results_proto_msgTypes[0] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use ScanFinding.ProtoReflect.Descriptor instead. func (*ScanFinding) Descriptor() ([]byte, []int) { return file_scan_results_proto_rawDescGZIP(), []int{0} } func (x *ScanFinding) GetTargetInfo() *TargetInfo { if x != nil { return x.TargetInfo } return nil } func (x *ScanFinding) GetNetworkService() *NetworkService { if x != nil { return x.NetworkService } return nil } func (x *ScanFinding) GetVulnerability() *Vulnerability { if x != nil { return x.Vulnerability } return nil } // Full scanning results. // NEXT ID: 9 type ScanResults struct { state protoimpl.MessageState `protogen:"open.v1"` // Status of this scan. ScanStatus ScanStatus `protobuf:"varint,1,opt,name=scan_status,json=scanStatus,proto3,enum=tsunami.proto.ScanStatus" json:"scan_status,omitempty"` // Detailed message for the scan status. StatusMessage string `protobuf:"bytes,6,opt,name=status_message,json=statusMessage,proto3" json:"status_message,omitempty"` // Reports whether the target was alive during the scan. // A target is considered alive if at least one network service was identified // or at least one vulnerability was detected. TargetAlive bool `protobuf:"varint,8,opt,name=target_alive,json=targetAlive,proto3" json:"target_alive,omitempty"` // All findings from this scan. ScanFindings []*ScanFinding `protobuf:"bytes,2,rep,name=scan_findings,json=scanFindings,proto3" json:"scan_findings,omitempty"` // Time when this scan was started. ScanStartTimestamp *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=scan_start_timestamp,json=scanStartTimestamp,proto3" json:"scan_start_timestamp,omitempty"` // Duration of the full scan. ScanDuration *durationpb.Duration `protobuf:"bytes,4,opt,name=scan_duration,json=scanDuration,proto3" json:"scan_duration,omitempty"` // Detection reports from all triggered Tsunami detection plugins. FullDetectionReports *FullDetectionReports `protobuf:"bytes,5,opt,name=full_detection_reports,json=fullDetectionReports,proto3" json:"full_detection_reports,omitempty"` // Reconnaissance reports from the fingerprinting stage. ReconnaissanceReport *ReconnaissanceReport `protobuf:"bytes,7,opt,name=reconnaissance_report,json=reconnaissanceReport,proto3" json:"reconnaissance_report,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *ScanResults) Reset() { *x = ScanResults{} mi := &file_scan_results_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *ScanResults) String() string { return protoimpl.X.MessageStringOf(x) } func (*ScanResults) ProtoMessage() {} func (x *ScanResults) ProtoReflect() protoreflect.Message { mi := &file_scan_results_proto_msgTypes[1] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use ScanResults.ProtoReflect.Descriptor instead. func (*ScanResults) Descriptor() ([]byte, []int) { return file_scan_results_proto_rawDescGZIP(), []int{1} } func (x *ScanResults) GetScanStatus() ScanStatus { if x != nil { return x.ScanStatus } return ScanStatus_SCAN_STATUS_UNSPECIFIED } func (x *ScanResults) GetStatusMessage() string { if x != nil { return x.StatusMessage } return "" } func (x *ScanResults) GetTargetAlive() bool { if x != nil { return x.TargetAlive } return false } func (x *ScanResults) GetScanFindings() []*ScanFinding { if x != nil { return x.ScanFindings } return nil } func (x *ScanResults) GetScanStartTimestamp() *timestamppb.Timestamp { if x != nil { return x.ScanStartTimestamp } return nil } func (x *ScanResults) GetScanDuration() *durationpb.Duration { if x != nil { return x.ScanDuration } return nil } func (x *ScanResults) GetFullDetectionReports() *FullDetectionReports { if x != nil { return x.FullDetectionReports } return nil } func (x *ScanResults) GetReconnaissanceReport() *ReconnaissanceReport { if x != nil { return x.ReconnaissanceReport } return nil } // Full detection reports from all triggered Tsunami detection plugins. type FullDetectionReports struct { state protoimpl.MessageState `protogen:"open.v1"` DetectionReports []*DetectionReport `protobuf:"bytes,1,rep,name=detection_reports,json=detectionReports,proto3" json:"detection_reports,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *FullDetectionReports) Reset() { *x = FullDetectionReports{} mi := &file_scan_results_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *FullDetectionReports) String() string { return protoimpl.X.MessageStringOf(x) } func (*FullDetectionReports) ProtoMessage() {} func (x *FullDetectionReports) ProtoReflect() protoreflect.Message { mi := &file_scan_results_proto_msgTypes[2] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use FullDetectionReports.ProtoReflect.Descriptor instead. func (*FullDetectionReports) Descriptor() ([]byte, []int) { return file_scan_results_proto_rawDescGZIP(), []int{2} } func (x *FullDetectionReports) GetDetectionReports() []*DetectionReport { if x != nil { return x.DetectionReports } return nil } var File_scan_results_proto protoreflect.FileDescriptor var file_scan_results_proto_rawDesc = string([]byte{ 0x0a, 0x12, 0x73, 0x63, 0x61, 0x6e, 0x5f, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0d, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x0f, 0x64, 0x65, 0x74, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x15, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x14, 0x72, 0x65, 0x63, 0x6f, 0x6e, 0x6e, 0x61, 0x69, 0x73, 0x73, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x13, 0x76, 0x75, 0x6c, 0x6e, 0x65, 0x72, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xd5, 0x01, 0x0a, 0x0b, 0x53, 0x63, 0x61, 0x6e, 0x46, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x12, 0x3a, 0x0a, 0x0b, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0a, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x46, 0x0a, 0x0f, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x0e, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x42, 0x0a, 0x0d, 0x76, 0x75, 0x6c, 0x6e, 0x65, 0x72, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x56, 0x75, 0x6c, 0x6e, 0x65, 0x72, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x52, 0x0d, 0x76, 0x75, 0x6c, 0x6e, 0x65, 0x72, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x22, 0x97, 0x04, 0x0a, 0x0b, 0x53, 0x63, 0x61, 0x6e, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x12, 0x3a, 0x0a, 0x0b, 0x73, 0x63, 0x61, 0x6e, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x19, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x53, 0x63, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x0a, 0x73, 0x63, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x25, 0x0a, 0x0e, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x61, 0x6c, 0x69, 0x76, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x41, 0x6c, 0x69, 0x76, 0x65, 0x12, 0x3f, 0x0a, 0x0d, 0x73, 0x63, 0x61, 0x6e, 0x5f, 0x66, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x53, 0x63, 0x61, 0x6e, 0x46, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x0c, 0x73, 0x63, 0x61, 0x6e, 0x46, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x4c, 0x0a, 0x14, 0x73, 0x63, 0x61, 0x6e, 0x5f, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x12, 0x73, 0x63, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x3e, 0x0a, 0x0d, 0x73, 0x63, 0x61, 0x6e, 0x5f, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0c, 0x73, 0x63, 0x61, 0x6e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x59, 0x0a, 0x16, 0x66, 0x75, 0x6c, 0x6c, 0x5f, 0x64, 0x65, 0x74, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x46, 0x75, 0x6c, 0x6c, 0x44, 0x65, 0x74, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x73, 0x52, 0x14, 0x66, 0x75, 0x6c, 0x6c, 0x44, 0x65, 0x74, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x73, 0x12, 0x58, 0x0a, 0x15, 0x72, 0x65, 0x63, 0x6f, 0x6e, 0x6e, 0x61, 0x69, 0x73, 0x73, 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x6e, 0x6e, 0x61, 0x69, 0x73, 0x73, 0x61, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x14, 0x72, 0x65, 0x63, 0x6f, 0x6e, 0x6e, 0x61, 0x69, 0x73, 0x73, 0x61, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x22, 0x63, 0x0a, 0x14, 0x46, 0x75, 0x6c, 0x6c, 0x44, 0x65, 0x74, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x73, 0x12, 0x4b, 0x0a, 0x11, 0x64, 0x65, 0x74, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x44, 0x65, 0x74, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x10, 0x64, 0x65, 0x74, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x73, 0x2a, 0x6c, 0x0a, 0x0a, 0x53, 0x63, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1b, 0x0a, 0x17, 0x53, 0x43, 0x41, 0x4e, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0d, 0x0a, 0x09, 0x53, 0x55, 0x43, 0x43, 0x45, 0x45, 0x44, 0x45, 0x44, 0x10, 0x01, 0x12, 0x17, 0x0a, 0x13, 0x50, 0x41, 0x52, 0x54, 0x49, 0x41, 0x4c, 0x4c, 0x59, 0x5f, 0x53, 0x55, 0x43, 0x43, 0x45, 0x45, 0x44, 0x45, 0x44, 0x10, 0x04, 0x12, 0x0a, 0x0a, 0x06, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x10, 0x02, 0x12, 0x0d, 0x0a, 0x09, 0x43, 0x41, 0x4e, 0x43, 0x45, 0x4c, 0x4c, 0x45, 0x44, 0x10, 0x03, 0x42, 0x72, 0x0a, 0x18, 0x63, 0x6f, 0x6d, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x42, 0x11, 0x53, 0x63, 0x61, 0x6e, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x50, 0x01, 0x5a, 0x41, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2d, 0x73, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x2d, 0x73, 0x63, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x5f, 0x67, 0x6f, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, }) var ( file_scan_results_proto_rawDescOnce sync.Once file_scan_results_proto_rawDescData []byte ) func file_scan_results_proto_rawDescGZIP() []byte { file_scan_results_proto_rawDescOnce.Do(func() { file_scan_results_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_scan_results_proto_rawDesc), len(file_scan_results_proto_rawDesc))) }) return file_scan_results_proto_rawDescData } var file_scan_results_proto_enumTypes = make([]protoimpl.EnumInfo, 1) var file_scan_results_proto_msgTypes = make([]protoimpl.MessageInfo, 3) var file_scan_results_proto_goTypes = []any{ (ScanStatus)(0), // 0: tsunami.proto.ScanStatus (*ScanFinding)(nil), // 1: tsunami.proto.ScanFinding (*ScanResults)(nil), // 2: tsunami.proto.ScanResults (*FullDetectionReports)(nil), // 3: tsunami.proto.FullDetectionReports (*TargetInfo)(nil), // 4: tsunami.proto.TargetInfo (*NetworkService)(nil), // 5: tsunami.proto.NetworkService (*Vulnerability)(nil), // 6: tsunami.proto.Vulnerability (*timestamppb.Timestamp)(nil), // 7: google.protobuf.Timestamp (*durationpb.Duration)(nil), // 8: google.protobuf.Duration (*ReconnaissanceReport)(nil), // 9: tsunami.proto.ReconnaissanceReport (*DetectionReport)(nil), // 10: tsunami.proto.DetectionReport } var file_scan_results_proto_depIdxs = []int32{ 4, // 0: tsunami.proto.ScanFinding.target_info:type_name -> tsunami.proto.TargetInfo 5, // 1: tsunami.proto.ScanFinding.network_service:type_name -> tsunami.proto.NetworkService 6, // 2: tsunami.proto.ScanFinding.vulnerability:type_name -> tsunami.proto.Vulnerability 0, // 3: tsunami.proto.ScanResults.scan_status:type_name -> tsunami.proto.ScanStatus 1, // 4: tsunami.proto.ScanResults.scan_findings:type_name -> tsunami.proto.ScanFinding 7, // 5: tsunami.proto.ScanResults.scan_start_timestamp:type_name -> google.protobuf.Timestamp 8, // 6: tsunami.proto.ScanResults.scan_duration:type_name -> google.protobuf.Duration 3, // 7: tsunami.proto.ScanResults.full_detection_reports:type_name -> tsunami.proto.FullDetectionReports 9, // 8: tsunami.proto.ScanResults.reconnaissance_report:type_name -> tsunami.proto.ReconnaissanceReport 10, // 9: tsunami.proto.FullDetectionReports.detection_reports:type_name -> tsunami.proto.DetectionReport 10, // [10:10] is the sub-list for method output_type 10, // [10:10] is the sub-list for method input_type 10, // [10:10] is the sub-list for extension type_name 10, // [10:10] is the sub-list for extension extendee 0, // [0:10] is the sub-list for field type_name } func init() { file_scan_results_proto_init() } func file_scan_results_proto_init() { if File_scan_results_proto != nil { return } file_detection_proto_init() file_network_service_proto_init() file_reconnaissance_proto_init() file_vulnerability_proto_init() type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_scan_results_proto_rawDesc), len(file_scan_results_proto_rawDesc)), NumEnums: 1, NumMessages: 3, NumExtensions: 0, NumServices: 0, }, GoTypes: file_scan_results_proto_goTypes, DependencyIndexes: file_scan_results_proto_depIdxs, EnumInfos: file_scan_results_proto_enumTypes, MessageInfos: file_scan_results_proto_msgTypes, }.Build() File_scan_results_proto = out.File file_scan_results_proto_goTypes = nil file_scan_results_proto_depIdxs = nil } ================================================ FILE: proto/tsunami_go_proto/scan_target.pb.go ================================================ // // Copyright 2020 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Data models for describing a scanning target. // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.5 // protoc v3.21.12 // source: scan_target.proto package tsunami_go_proto import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" unsafe "unsafe" ) const ( // Verify that this generated code is sufficiently up-to-date. _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) // Verify that runtime/protoimpl is sufficiently up-to-date. _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) // The information about a scan target. type ScanTarget struct { state protoimpl.MessageState `protogen:"open.v1"` // Types that are valid to be assigned to Target: // // *ScanTarget_NetworkEndpoint // *ScanTarget_NetworkService Target isScanTarget_Target `protobuf_oneof:"target"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *ScanTarget) Reset() { *x = ScanTarget{} mi := &file_scan_target_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *ScanTarget) String() string { return protoimpl.X.MessageStringOf(x) } func (*ScanTarget) ProtoMessage() {} func (x *ScanTarget) ProtoReflect() protoreflect.Message { mi := &file_scan_target_proto_msgTypes[0] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use ScanTarget.ProtoReflect.Descriptor instead. func (*ScanTarget) Descriptor() ([]byte, []int) { return file_scan_target_proto_rawDescGZIP(), []int{0} } func (x *ScanTarget) GetTarget() isScanTarget_Target { if x != nil { return x.Target } return nil } func (x *ScanTarget) GetNetworkEndpoint() *NetworkEndpoint { if x != nil { if x, ok := x.Target.(*ScanTarget_NetworkEndpoint); ok { return x.NetworkEndpoint } } return nil } func (x *ScanTarget) GetNetworkService() *NetworkService { if x != nil { if x, ok := x.Target.(*ScanTarget_NetworkService); ok { return x.NetworkService } } return nil } type isScanTarget_Target interface { isScanTarget_Target() } type ScanTarget_NetworkEndpoint struct { // The network endpoint to be scanned. NetworkEndpoint *NetworkEndpoint `protobuf:"bytes,1,opt,name=network_endpoint,json=networkEndpoint,proto3,oneof"` } type ScanTarget_NetworkService struct { // The network service to be scanned. NetworkService *NetworkService `protobuf:"bytes,2,opt,name=network_service,json=networkService,proto3,oneof"` } func (*ScanTarget_NetworkEndpoint) isScanTarget_Target() {} func (*ScanTarget_NetworkService) isScanTarget_Target() {} var File_scan_target_proto protoreflect.FileDescriptor var file_scan_target_proto_rawDesc = string([]byte{ 0x0a, 0x11, 0x73, 0x63, 0x61, 0x6e, 0x5f, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0d, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x0d, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x15, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xad, 0x01, 0x0a, 0x0a, 0x53, 0x63, 0x61, 0x6e, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x12, 0x4b, 0x0a, 0x10, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x5f, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x48, 0x00, 0x52, 0x0f, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x48, 0x0a, 0x0f, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x48, 0x00, 0x52, 0x0e, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x42, 0x08, 0x0a, 0x06, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x42, 0x71, 0x0a, 0x18, 0x63, 0x6f, 0x6d, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x42, 0x10, 0x53, 0x63, 0x61, 0x6e, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x50, 0x01, 0x5a, 0x41, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2d, 0x73, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x2d, 0x73, 0x63, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x5f, 0x67, 0x6f, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, }) var ( file_scan_target_proto_rawDescOnce sync.Once file_scan_target_proto_rawDescData []byte ) func file_scan_target_proto_rawDescGZIP() []byte { file_scan_target_proto_rawDescOnce.Do(func() { file_scan_target_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_scan_target_proto_rawDesc), len(file_scan_target_proto_rawDesc))) }) return file_scan_target_proto_rawDescData } var file_scan_target_proto_msgTypes = make([]protoimpl.MessageInfo, 1) var file_scan_target_proto_goTypes = []any{ (*ScanTarget)(nil), // 0: tsunami.proto.ScanTarget (*NetworkEndpoint)(nil), // 1: tsunami.proto.NetworkEndpoint (*NetworkService)(nil), // 2: tsunami.proto.NetworkService } var file_scan_target_proto_depIdxs = []int32{ 1, // 0: tsunami.proto.ScanTarget.network_endpoint:type_name -> tsunami.proto.NetworkEndpoint 2, // 1: tsunami.proto.ScanTarget.network_service:type_name -> tsunami.proto.NetworkService 2, // [2:2] is the sub-list for method output_type 2, // [2:2] is the sub-list for method input_type 2, // [2:2] is the sub-list for extension type_name 2, // [2:2] is the sub-list for extension extendee 0, // [0:2] is the sub-list for field type_name } func init() { file_scan_target_proto_init() } func file_scan_target_proto_init() { if File_scan_target_proto != nil { return } file_network_proto_init() file_network_service_proto_init() file_scan_target_proto_msgTypes[0].OneofWrappers = []any{ (*ScanTarget_NetworkEndpoint)(nil), (*ScanTarget_NetworkService)(nil), } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_scan_target_proto_rawDesc), len(file_scan_target_proto_rawDesc)), NumEnums: 0, NumMessages: 1, NumExtensions: 0, NumServices: 0, }, GoTypes: file_scan_target_proto_goTypes, DependencyIndexes: file_scan_target_proto_depIdxs, MessageInfos: file_scan_target_proto_msgTypes, }.Build() File_scan_target_proto = out.File file_scan_target_proto_goTypes = nil file_scan_target_proto_depIdxs = nil } ================================================ FILE: proto/tsunami_go_proto/software.pb.go ================================================ // // Copyright 2019 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Data models for describing a software. // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.5 // protoc v3.21.12 // source: software.proto package tsunami_go_proto import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" unsafe "unsafe" ) const ( // Verify that this generated code is sufficiently up-to-date. _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) // Verify that runtime/protoimpl is sufficiently up-to-date. _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) // Type of the Version message, identifying an ordinary software version or a // sentinel MINIMUM/MAXIMUM version. See comments below for what is a sentinel // version. type Version_VersionType int32 const ( Version_VERSION_TYPE_UNSPECIFIED Version_VersionType = 0 // A normal software version. Version_NORMAL Version_VersionType = 1 // A sentinel version representing negative infinity, i.e. MINIMUM version // is less than any NORMAL and MAXIMUM versions. Version_MINIMUM Version_VersionType = 2 // A sentinel version representing positive infinity, i.e. MAXIMUM version // is greater than any NORMAL and MINIMUM versions. Version_MAXIMUM Version_VersionType = 3 ) // Enum value maps for Version_VersionType. var ( Version_VersionType_name = map[int32]string{ 0: "VERSION_TYPE_UNSPECIFIED", 1: "NORMAL", 2: "MINIMUM", 3: "MAXIMUM", } Version_VersionType_value = map[string]int32{ "VERSION_TYPE_UNSPECIFIED": 0, "NORMAL": 1, "MINIMUM": 2, "MAXIMUM": 3, } ) func (x Version_VersionType) Enum() *Version_VersionType { p := new(Version_VersionType) *p = x return p } func (x Version_VersionType) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (Version_VersionType) Descriptor() protoreflect.EnumDescriptor { return file_software_proto_enumTypes[0].Descriptor() } func (Version_VersionType) Type() protoreflect.EnumType { return &file_software_proto_enumTypes[0] } func (x Version_VersionType) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } // Deprecated: Use Version_VersionType.Descriptor instead. func (Version_VersionType) EnumDescriptor() ([]byte, []int) { return file_software_proto_rawDescGZIP(), []int{0, 0} } // Whether the range endpoint is inclusive or exclusive. type VersionRange_Inclusiveness int32 const ( VersionRange_INCLUSIVENESS_UNSPECIFIED VersionRange_Inclusiveness = 0 VersionRange_INCLUSIVE VersionRange_Inclusiveness = 1 VersionRange_EXCLUSIVE VersionRange_Inclusiveness = 2 ) // Enum value maps for VersionRange_Inclusiveness. var ( VersionRange_Inclusiveness_name = map[int32]string{ 0: "INCLUSIVENESS_UNSPECIFIED", 1: "INCLUSIVE", 2: "EXCLUSIVE", } VersionRange_Inclusiveness_value = map[string]int32{ "INCLUSIVENESS_UNSPECIFIED": 0, "INCLUSIVE": 1, "EXCLUSIVE": 2, } ) func (x VersionRange_Inclusiveness) Enum() *VersionRange_Inclusiveness { p := new(VersionRange_Inclusiveness) *p = x return p } func (x VersionRange_Inclusiveness) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (VersionRange_Inclusiveness) Descriptor() protoreflect.EnumDescriptor { return file_software_proto_enumTypes[1].Descriptor() } func (VersionRange_Inclusiveness) Type() protoreflect.EnumType { return &file_software_proto_enumTypes[1] } func (x VersionRange_Inclusiveness) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } // Deprecated: Use VersionRange_Inclusiveness.Descriptor instead. func (VersionRange_Inclusiveness) EnumDescriptor() ([]byte, []int) { return file_software_proto_rawDescGZIP(), []int{1, 0} } // The exact version of a software. type Version struct { state protoimpl.MessageState `protogen:"open.v1"` // Distinguishes between sentinel MIN/MAX versions and normal versions. Type Version_VersionType `protobuf:"varint,1,opt,name=type,proto3,enum=tsunami.proto.Version_VersionType" json:"type,omitempty"` // Human readable version number, e.g. 1.0.3. This is set only when type is // NORMAL. Tsunami uses raw string to represent a version number instead of // any structured messages in order to handle different kinds of version // schemes. Tsunami will tokenize this version string and store tokens // internally. When performing version comparisons, Tsunami follows the // precedence defined by Semantic Versioning (semver.org). More details can be // found in Tsunami's internal Version class. FullVersionString string `protobuf:"bytes,2,opt,name=full_version_string,json=fullVersionString,proto3" json:"full_version_string,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *Version) Reset() { *x = Version{} mi := &file_software_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *Version) String() string { return protoimpl.X.MessageStringOf(x) } func (*Version) ProtoMessage() {} func (x *Version) ProtoReflect() protoreflect.Message { mi := &file_software_proto_msgTypes[0] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Version.ProtoReflect.Descriptor instead. func (*Version) Descriptor() ([]byte, []int) { return file_software_proto_rawDescGZIP(), []int{0} } func (x *Version) GetType() Version_VersionType { if x != nil { return x.Type } return Version_VERSION_TYPE_UNSPECIFIED } func (x *Version) GetFullVersionString() string { if x != nil { return x.FullVersionString } return "" } // An inclusive range of versions for a software. type VersionRange struct { state protoimpl.MessageState `protogen:"open.v1"` // Minimum version that belongs in the range. MinVersion *Version `protobuf:"bytes,1,opt,name=min_version,json=minVersion,proto3" json:"min_version,omitempty"` // Inclusiveness of the min_version. When min_version points to negative // infinity, this value will always be EXCLUSIVE to matching the // representation of (-inf, 1.0]. Note that negative infinity version should // ***NOT*** be compared with a version range as it is just a bogus sentinel // version without any meaning. MinVersionInclusiveness VersionRange_Inclusiveness `protobuf:"varint,2,opt,name=min_version_inclusiveness,json=minVersionInclusiveness,proto3,enum=tsunami.proto.VersionRange_Inclusiveness" json:"min_version_inclusiveness,omitempty"` // Maximum version that belongs in the range. MaxVersion *Version `protobuf:"bytes,3,opt,name=max_version,json=maxVersion,proto3" json:"max_version,omitempty"` // Inclusiveness of the max_version. When max_version points to positive // infinity, this value will always be EXCLUSIVE to matching the // representation of [1.0, inf). Note that positive infinity version should // ***NOT*** be compared with a version range as it is just a bogus sentinel // version without any meaning. MaxVersionInclusiveness VersionRange_Inclusiveness `protobuf:"varint,4,opt,name=max_version_inclusiveness,json=maxVersionInclusiveness,proto3,enum=tsunami.proto.VersionRange_Inclusiveness" json:"max_version_inclusiveness,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *VersionRange) Reset() { *x = VersionRange{} mi := &file_software_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *VersionRange) String() string { return protoimpl.X.MessageStringOf(x) } func (*VersionRange) ProtoMessage() {} func (x *VersionRange) ProtoReflect() protoreflect.Message { mi := &file_software_proto_msgTypes[1] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use VersionRange.ProtoReflect.Descriptor instead. func (*VersionRange) Descriptor() ([]byte, []int) { return file_software_proto_rawDescGZIP(), []int{1} } func (x *VersionRange) GetMinVersion() *Version { if x != nil { return x.MinVersion } return nil } func (x *VersionRange) GetMinVersionInclusiveness() VersionRange_Inclusiveness { if x != nil { return x.MinVersionInclusiveness } return VersionRange_INCLUSIVENESS_UNSPECIFIED } func (x *VersionRange) GetMaxVersion() *Version { if x != nil { return x.MaxVersion } return nil } func (x *VersionRange) GetMaxVersionInclusiveness() VersionRange_Inclusiveness { if x != nil { return x.MaxVersionInclusiveness } return VersionRange_INCLUSIVENESS_UNSPECIFIED } // A set of Versions and VersionRanges that completely describes a set of // software releases, e.g. {3.9.1, 3.9.3, [4.7.1, 4.7.8], 4.8} type VersionSet struct { state protoimpl.MessageState `protogen:"open.v1"` Versions []*Version `protobuf:"bytes,1,rep,name=versions,proto3" json:"versions,omitempty"` VersionRanges []*VersionRange `protobuf:"bytes,2,rep,name=version_ranges,json=versionRanges,proto3" json:"version_ranges,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *VersionSet) Reset() { *x = VersionSet{} mi := &file_software_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *VersionSet) String() string { return protoimpl.X.MessageStringOf(x) } func (*VersionSet) ProtoMessage() {} func (x *VersionSet) ProtoReflect() protoreflect.Message { mi := &file_software_proto_msgTypes[2] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use VersionSet.ProtoReflect.Descriptor instead. func (*VersionSet) Descriptor() ([]byte, []int) { return file_software_proto_rawDescGZIP(), []int{2} } func (x *VersionSet) GetVersions() []*Version { if x != nil { return x.Versions } return nil } func (x *VersionSet) GetVersionRanges() []*VersionRange { if x != nil { return x.VersionRanges } return nil } // A structured description about a software. type Software struct { state protoimpl.MessageState `protogen:"open.v1"` // The name of this software. Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *Software) Reset() { *x = Software{} mi := &file_software_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *Software) String() string { return protoimpl.X.MessageStringOf(x) } func (*Software) ProtoMessage() {} func (x *Software) ProtoReflect() protoreflect.Message { mi := &file_software_proto_msgTypes[3] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Software.ProtoReflect.Descriptor instead. func (*Software) Descriptor() ([]byte, []int) { return file_software_proto_rawDescGZIP(), []int{3} } func (x *Software) GetName() string { if x != nil { return x.Name } return "" } var File_software_proto protoreflect.FileDescriptor var file_software_proto_rawDesc = string([]byte{ 0x0a, 0x0e, 0x73, 0x6f, 0x66, 0x74, 0x77, 0x61, 0x72, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0d, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xc4, 0x01, 0x0a, 0x07, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x36, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x22, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x2e, 0x0a, 0x13, 0x66, 0x75, 0x6c, 0x6c, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x66, 0x75, 0x6c, 0x6c, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x22, 0x51, 0x0a, 0x0b, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1c, 0x0a, 0x18, 0x56, 0x45, 0x52, 0x53, 0x49, 0x4f, 0x4e, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x4e, 0x4f, 0x52, 0x4d, 0x41, 0x4c, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x4d, 0x49, 0x4e, 0x49, 0x4d, 0x55, 0x4d, 0x10, 0x02, 0x12, 0x0b, 0x0a, 0x07, 0x4d, 0x41, 0x58, 0x49, 0x4d, 0x55, 0x4d, 0x10, 0x03, 0x22, 0x9c, 0x03, 0x0a, 0x0c, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x37, 0x0a, 0x0b, 0x6d, 0x69, 0x6e, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x6d, 0x69, 0x6e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x65, 0x0a, 0x19, 0x6d, 0x69, 0x6e, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x73, 0x69, 0x76, 0x65, 0x6e, 0x65, 0x73, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x29, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x2e, 0x49, 0x6e, 0x63, 0x6c, 0x75, 0x73, 0x69, 0x76, 0x65, 0x6e, 0x65, 0x73, 0x73, 0x52, 0x17, 0x6d, 0x69, 0x6e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x6e, 0x63, 0x6c, 0x75, 0x73, 0x69, 0x76, 0x65, 0x6e, 0x65, 0x73, 0x73, 0x12, 0x37, 0x0a, 0x0b, 0x6d, 0x61, 0x78, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x6d, 0x61, 0x78, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x65, 0x0a, 0x19, 0x6d, 0x61, 0x78, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x73, 0x69, 0x76, 0x65, 0x6e, 0x65, 0x73, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x29, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x2e, 0x49, 0x6e, 0x63, 0x6c, 0x75, 0x73, 0x69, 0x76, 0x65, 0x6e, 0x65, 0x73, 0x73, 0x52, 0x17, 0x6d, 0x61, 0x78, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x6e, 0x63, 0x6c, 0x75, 0x73, 0x69, 0x76, 0x65, 0x6e, 0x65, 0x73, 0x73, 0x22, 0x4c, 0x0a, 0x0d, 0x49, 0x6e, 0x63, 0x6c, 0x75, 0x73, 0x69, 0x76, 0x65, 0x6e, 0x65, 0x73, 0x73, 0x12, 0x1d, 0x0a, 0x19, 0x49, 0x4e, 0x43, 0x4c, 0x55, 0x53, 0x49, 0x56, 0x45, 0x4e, 0x45, 0x53, 0x53, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0d, 0x0a, 0x09, 0x49, 0x4e, 0x43, 0x4c, 0x55, 0x53, 0x49, 0x56, 0x45, 0x10, 0x01, 0x12, 0x0d, 0x0a, 0x09, 0x45, 0x58, 0x43, 0x4c, 0x55, 0x53, 0x49, 0x56, 0x45, 0x10, 0x02, 0x22, 0x84, 0x01, 0x0a, 0x0a, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x12, 0x32, 0x0a, 0x08, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x08, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x42, 0x0a, 0x0e, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x0d, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x22, 0x1e, 0x0a, 0x08, 0x53, 0x6f, 0x66, 0x74, 0x77, 0x61, 0x72, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x42, 0x6f, 0x0a, 0x18, 0x63, 0x6f, 0x6d, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x42, 0x0e, 0x53, 0x6f, 0x66, 0x74, 0x77, 0x61, 0x72, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x50, 0x01, 0x5a, 0x41, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2d, 0x73, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x2d, 0x73, 0x63, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x5f, 0x67, 0x6f, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, }) var ( file_software_proto_rawDescOnce sync.Once file_software_proto_rawDescData []byte ) func file_software_proto_rawDescGZIP() []byte { file_software_proto_rawDescOnce.Do(func() { file_software_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_software_proto_rawDesc), len(file_software_proto_rawDesc))) }) return file_software_proto_rawDescData } var file_software_proto_enumTypes = make([]protoimpl.EnumInfo, 2) var file_software_proto_msgTypes = make([]protoimpl.MessageInfo, 4) var file_software_proto_goTypes = []any{ (Version_VersionType)(0), // 0: tsunami.proto.Version.VersionType (VersionRange_Inclusiveness)(0), // 1: tsunami.proto.VersionRange.Inclusiveness (*Version)(nil), // 2: tsunami.proto.Version (*VersionRange)(nil), // 3: tsunami.proto.VersionRange (*VersionSet)(nil), // 4: tsunami.proto.VersionSet (*Software)(nil), // 5: tsunami.proto.Software } var file_software_proto_depIdxs = []int32{ 0, // 0: tsunami.proto.Version.type:type_name -> tsunami.proto.Version.VersionType 2, // 1: tsunami.proto.VersionRange.min_version:type_name -> tsunami.proto.Version 1, // 2: tsunami.proto.VersionRange.min_version_inclusiveness:type_name -> tsunami.proto.VersionRange.Inclusiveness 2, // 3: tsunami.proto.VersionRange.max_version:type_name -> tsunami.proto.Version 1, // 4: tsunami.proto.VersionRange.max_version_inclusiveness:type_name -> tsunami.proto.VersionRange.Inclusiveness 2, // 5: tsunami.proto.VersionSet.versions:type_name -> tsunami.proto.Version 3, // 6: tsunami.proto.VersionSet.version_ranges:type_name -> tsunami.proto.VersionRange 7, // [7:7] is the sub-list for method output_type 7, // [7:7] is the sub-list for method input_type 7, // [7:7] is the sub-list for extension type_name 7, // [7:7] is the sub-list for extension extendee 0, // [0:7] is the sub-list for field type_name } func init() { file_software_proto_init() } func file_software_proto_init() { if File_software_proto != nil { return } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_software_proto_rawDesc), len(file_software_proto_rawDesc)), NumEnums: 2, NumMessages: 4, NumExtensions: 0, NumServices: 0, }, GoTypes: file_software_proto_goTypes, DependencyIndexes: file_software_proto_depIdxs, EnumInfos: file_software_proto_enumTypes, MessageInfos: file_software_proto_msgTypes, }.Build() File_software_proto = out.File file_software_proto_goTypes = nil file_software_proto_depIdxs = nil } ================================================ FILE: proto/tsunami_go_proto/vulnerability.pb.go ================================================ // // Copyright 2020 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Data models for describing a vulnerability. // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.5 // protoc v3.21.12 // source: vulnerability.proto package tsunami_go_proto import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" unsafe "unsafe" ) const ( // Verify that this generated code is sufficiently up-to-date. _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) // Verify that runtime/protoimpl is sufficiently up-to-date. _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) // Severity of a vulnerability. type Severity int32 const ( // Unspecified severity. Severity_SEVERITY_UNSPECIFIED Severity = 0 // Minimal severity. Severity_MINIMAL Severity = 1 // Low severity. Severity_LOW Severity = 2 // Medium severity. Severity_MEDIUM Severity = 3 // High severity. Severity_HIGH Severity = 4 // Critical severity. Severity_CRITICAL Severity = 5 ) // Enum value maps for Severity. var ( Severity_name = map[int32]string{ 0: "SEVERITY_UNSPECIFIED", 1: "MINIMAL", 2: "LOW", 3: "MEDIUM", 4: "HIGH", 5: "CRITICAL", } Severity_value = map[string]int32{ "SEVERITY_UNSPECIFIED": 0, "MINIMAL": 1, "LOW": 2, "MEDIUM": 3, "HIGH": 4, "CRITICAL": 5, } ) func (x Severity) Enum() *Severity { p := new(Severity) *p = x return p } func (x Severity) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (Severity) Descriptor() protoreflect.EnumDescriptor { return file_vulnerability_proto_enumTypes[0].Descriptor() } func (Severity) Type() protoreflect.EnumType { return &file_vulnerability_proto_enumTypes[0] } func (x Severity) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } // Deprecated: Use Severity.Descriptor instead. func (Severity) EnumDescriptor() ([]byte, []int) { return file_vulnerability_proto_rawDescGZIP(), []int{0} } // The identifier that uniquely identifies this vulnerability. type VulnerabilityId struct { state protoimpl.MessageState `protogen:"open.v1"` // Entity that published this identifier. Publisher string `protobuf:"bytes,1,opt,name=publisher,proto3" json:"publisher,omitempty"` // Publisher assigned unique identifier. Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` // Optional. URL for details about this vulnerability. Link string `protobuf:"bytes,3,opt,name=link,proto3" json:"link,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *VulnerabilityId) Reset() { *x = VulnerabilityId{} mi := &file_vulnerability_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *VulnerabilityId) String() string { return protoimpl.X.MessageStringOf(x) } func (*VulnerabilityId) ProtoMessage() {} func (x *VulnerabilityId) ProtoReflect() protoreflect.Message { mi := &file_vulnerability_proto_msgTypes[0] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use VulnerabilityId.ProtoReflect.Descriptor instead. func (*VulnerabilityId) Descriptor() ([]byte, []int) { return file_vulnerability_proto_rawDescGZIP(), []int{0} } func (x *VulnerabilityId) GetPublisher() string { if x != nil { return x.Publisher } return "" } func (x *VulnerabilityId) GetValue() string { if x != nil { return x.Value } return "" } func (x *VulnerabilityId) GetLink() string { if x != nil { return x.Link } return "" } // Message that represents one single vulnerability detected by Tsunami. type Vulnerability struct { state protoimpl.MessageState `protogen:"open.v1"` // The main identifier for this vulnerability, usually a publicly known // identifier like CVEs and such. If not publicly known, users are expected to // assign an id on their own. MainId *VulnerabilityId `protobuf:"bytes,1,opt,name=main_id,json=mainId,proto3" json:"main_id,omitempty"` // Any related identifiers about this vulnerability, e.g. a CWE weakness. RelatedId []*VulnerabilityId `protobuf:"bytes,2,rep,name=related_id,json=relatedId,proto3" json:"related_id,omitempty"` // Severity of this vulnerability. Severity Severity `protobuf:"varint,3,opt,name=severity,proto3,enum=tsunami.proto.Severity" json:"severity,omitempty"` // Terse but descriptive sentence about this vulnerability. // For example: "Default Password (0p3nm35h) for 'root' Account.". Title string `protobuf:"bytes,4,opt,name=title,proto3" json:"title,omitempty"` // Verbose description of this vulnerability. Description string `protobuf:"bytes,5,opt,name=description,proto3" json:"description,omitempty"` // Optional. Verbose recommended solution(s). Recommendation string `protobuf:"bytes,6,opt,name=recommendation,proto3" json:"recommendation,omitempty"` // Optional. The CVSS v2 score of this vulnerability. CvssV2 string `protobuf:"bytes,7,opt,name=cvss_v2,json=cvssV2,proto3" json:"cvss_v2,omitempty"` // Optional. The CVSS v3 score of this vulnerability. CvssV3 string `protobuf:"bytes,8,opt,name=cvss_v3,json=cvssV3,proto3" json:"cvss_v3,omitempty"` // Any additional technical details about this vulnerability. AdditionalDetails []*AdditionalDetail `protobuf:"bytes,9,rep,name=additional_details,json=additionalDetails,proto3" json:"additional_details,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *Vulnerability) Reset() { *x = Vulnerability{} mi := &file_vulnerability_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *Vulnerability) String() string { return protoimpl.X.MessageStringOf(x) } func (*Vulnerability) ProtoMessage() {} func (x *Vulnerability) ProtoReflect() protoreflect.Message { mi := &file_vulnerability_proto_msgTypes[1] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Vulnerability.ProtoReflect.Descriptor instead. func (*Vulnerability) Descriptor() ([]byte, []int) { return file_vulnerability_proto_rawDescGZIP(), []int{1} } func (x *Vulnerability) GetMainId() *VulnerabilityId { if x != nil { return x.MainId } return nil } func (x *Vulnerability) GetRelatedId() []*VulnerabilityId { if x != nil { return x.RelatedId } return nil } func (x *Vulnerability) GetSeverity() Severity { if x != nil { return x.Severity } return Severity_SEVERITY_UNSPECIFIED } func (x *Vulnerability) GetTitle() string { if x != nil { return x.Title } return "" } func (x *Vulnerability) GetDescription() string { if x != nil { return x.Description } return "" } func (x *Vulnerability) GetRecommendation() string { if x != nil { return x.Recommendation } return "" } func (x *Vulnerability) GetCvssV2() string { if x != nil { return x.CvssV2 } return "" } func (x *Vulnerability) GetCvssV3() string { if x != nil { return x.CvssV3 } return "" } func (x *Vulnerability) GetAdditionalDetails() []*AdditionalDetail { if x != nil { return x.AdditionalDetails } return nil } // Additional details regarding a vulnerability can be stored here. Prefers to // use the existing structured data when possible, otherwise store the raw data // as a blob. type AdditionalDetail struct { state protoimpl.MessageState `protogen:"open.v1"` Description string `protobuf:"bytes,1,opt,name=description,proto3" json:"description,omitempty"` // Types that are valid to be assigned to Detail: // // *AdditionalDetail_BlobData // *AdditionalDetail_TextData // *AdditionalDetail_Credential // *AdditionalDetail_Credentials Detail isAdditionalDetail_Detail `protobuf_oneof:"detail"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *AdditionalDetail) Reset() { *x = AdditionalDetail{} mi := &file_vulnerability_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *AdditionalDetail) String() string { return protoimpl.X.MessageStringOf(x) } func (*AdditionalDetail) ProtoMessage() {} func (x *AdditionalDetail) ProtoReflect() protoreflect.Message { mi := &file_vulnerability_proto_msgTypes[2] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use AdditionalDetail.ProtoReflect.Descriptor instead. func (*AdditionalDetail) Descriptor() ([]byte, []int) { return file_vulnerability_proto_rawDescGZIP(), []int{2} } func (x *AdditionalDetail) GetDescription() string { if x != nil { return x.Description } return "" } func (x *AdditionalDetail) GetDetail() isAdditionalDetail_Detail { if x != nil { return x.Detail } return nil } func (x *AdditionalDetail) GetBlobData() *BlobData { if x != nil { if x, ok := x.Detail.(*AdditionalDetail_BlobData); ok { return x.BlobData } } return nil } func (x *AdditionalDetail) GetTextData() *TextData { if x != nil { if x, ok := x.Detail.(*AdditionalDetail_TextData); ok { return x.TextData } } return nil } func (x *AdditionalDetail) GetCredential() *Credential { if x != nil { if x, ok := x.Detail.(*AdditionalDetail_Credential); ok { return x.Credential } } return nil } func (x *AdditionalDetail) GetCredentials() *Credentials { if x != nil { if x, ok := x.Detail.(*AdditionalDetail_Credentials); ok { return x.Credentials } } return nil } type isAdditionalDetail_Detail interface { isAdditionalDetail_Detail() } type AdditionalDetail_BlobData struct { BlobData *BlobData `protobuf:"bytes,2,opt,name=blob_data,json=blobData,proto3,oneof"` } type AdditionalDetail_TextData struct { TextData *TextData `protobuf:"bytes,3,opt,name=text_data,json=textData,proto3,oneof"` } type AdditionalDetail_Credential struct { Credential *Credential `protobuf:"bytes,4,opt,name=credential,proto3,oneof"` } type AdditionalDetail_Credentials struct { Credentials *Credentials `protobuf:"bytes,5,opt,name=credentials,proto3,oneof"` } func (*AdditionalDetail_BlobData) isAdditionalDetail_Detail() {} func (*AdditionalDetail_TextData) isAdditionalDetail_Detail() {} func (*AdditionalDetail_Credential) isAdditionalDetail_Detail() {} func (*AdditionalDetail_Credentials) isAdditionalDetail_Detail() {} // A piece of arbitrary binary data. type BlobData struct { state protoimpl.MessageState `protogen:"open.v1"` Data []byte `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *BlobData) Reset() { *x = BlobData{} mi := &file_vulnerability_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *BlobData) String() string { return protoimpl.X.MessageStringOf(x) } func (*BlobData) ProtoMessage() {} func (x *BlobData) ProtoReflect() protoreflect.Message { mi := &file_vulnerability_proto_msgTypes[3] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use BlobData.ProtoReflect.Descriptor instead. func (*BlobData) Descriptor() ([]byte, []int) { return file_vulnerability_proto_rawDescGZIP(), []int{3} } func (x *BlobData) GetData() []byte { if x != nil { return x.Data } return nil } // A piece of arbitrary UTF-8 encoded text data. type TextData struct { state protoimpl.MessageState `protogen:"open.v1"` Text string `protobuf:"bytes,1,opt,name=text,proto3" json:"text,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *TextData) Reset() { *x = TextData{} mi := &file_vulnerability_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *TextData) String() string { return protoimpl.X.MessageStringOf(x) } func (*TextData) ProtoMessage() {} func (x *TextData) ProtoReflect() protoreflect.Message { mi := &file_vulnerability_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use TextData.ProtoReflect.Descriptor instead. func (*TextData) Descriptor() ([]byte, []int) { return file_vulnerability_proto_rawDescGZIP(), []int{4} } func (x *TextData) GetText() string { if x != nil { return x.Text } return "" } // Credential for a vulnerable network service. type Credential struct { state protoimpl.MessageState `protogen:"open.v1"` Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"` Password string `protobuf:"bytes,2,opt,name=password,proto3" json:"password,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *Credential) Reset() { *x = Credential{} mi := &file_vulnerability_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *Credential) String() string { return protoimpl.X.MessageStringOf(x) } func (*Credential) ProtoMessage() {} func (x *Credential) ProtoReflect() protoreflect.Message { mi := &file_vulnerability_proto_msgTypes[5] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Credential.ProtoReflect.Descriptor instead. func (*Credential) Descriptor() ([]byte, []int) { return file_vulnerability_proto_rawDescGZIP(), []int{5} } func (x *Credential) GetUsername() string { if x != nil { return x.Username } return "" } func (x *Credential) GetPassword() string { if x != nil { return x.Password } return "" } // A set of credentials for a vulnerable network service. type Credentials struct { state protoimpl.MessageState `protogen:"open.v1"` Credential []*Credential `protobuf:"bytes,1,rep,name=credential,proto3" json:"credential,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *Credentials) Reset() { *x = Credentials{} mi := &file_vulnerability_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *Credentials) String() string { return protoimpl.X.MessageStringOf(x) } func (*Credentials) ProtoMessage() {} func (x *Credentials) ProtoReflect() protoreflect.Message { mi := &file_vulnerability_proto_msgTypes[6] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Credentials.ProtoReflect.Descriptor instead. func (*Credentials) Descriptor() ([]byte, []int) { return file_vulnerability_proto_rawDescGZIP(), []int{6} } func (x *Credentials) GetCredential() []*Credential { if x != nil { return x.Credential } return nil } var File_vulnerability_proto protoreflect.FileDescriptor var file_vulnerability_proto_rawDesc = string([]byte{ 0x0a, 0x13, 0x76, 0x75, 0x6c, 0x6e, 0x65, 0x72, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0d, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x59, 0x0a, 0x0f, 0x56, 0x75, 0x6c, 0x6e, 0x65, 0x72, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x49, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x65, 0x72, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6c, 0x69, 0x6e, 0x6b, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6c, 0x69, 0x6e, 0x6b, 0x22, 0x9e, 0x03, 0x0a, 0x0d, 0x56, 0x75, 0x6c, 0x6e, 0x65, 0x72, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x12, 0x37, 0x0a, 0x07, 0x6d, 0x61, 0x69, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x56, 0x75, 0x6c, 0x6e, 0x65, 0x72, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x49, 0x64, 0x52, 0x06, 0x6d, 0x61, 0x69, 0x6e, 0x49, 0x64, 0x12, 0x3d, 0x0a, 0x0a, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x56, 0x75, 0x6c, 0x6e, 0x65, 0x72, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x49, 0x64, 0x52, 0x09, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x49, 0x64, 0x12, 0x33, 0x0a, 0x08, 0x73, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x17, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x53, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x52, 0x08, 0x73, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x26, 0x0a, 0x0e, 0x72, 0x65, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x72, 0x65, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x17, 0x0a, 0x07, 0x63, 0x76, 0x73, 0x73, 0x5f, 0x76, 0x32, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x63, 0x76, 0x73, 0x73, 0x56, 0x32, 0x12, 0x17, 0x0a, 0x07, 0x63, 0x76, 0x73, 0x73, 0x5f, 0x76, 0x33, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x63, 0x76, 0x73, 0x73, 0x56, 0x33, 0x12, 0x4e, 0x0a, 0x12, 0x61, 0x64, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x18, 0x09, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x64, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x52, 0x11, 0x61, 0x64, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x22, 0xab, 0x02, 0x0a, 0x10, 0x41, 0x64, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x36, 0x0a, 0x09, 0x62, 0x6c, 0x6f, 0x62, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x42, 0x6c, 0x6f, 0x62, 0x44, 0x61, 0x74, 0x61, 0x48, 0x00, 0x52, 0x08, 0x62, 0x6c, 0x6f, 0x62, 0x44, 0x61, 0x74, 0x61, 0x12, 0x36, 0x0a, 0x09, 0x74, 0x65, 0x78, 0x74, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x54, 0x65, 0x78, 0x74, 0x44, 0x61, 0x74, 0x61, 0x48, 0x00, 0x52, 0x08, 0x74, 0x65, 0x78, 0x74, 0x44, 0x61, 0x74, 0x61, 0x12, 0x3b, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x48, 0x00, 0x52, 0x0a, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x12, 0x3e, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x48, 0x00, 0x52, 0x0b, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x42, 0x08, 0x0a, 0x06, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x22, 0x1e, 0x0a, 0x08, 0x42, 0x6c, 0x6f, 0x62, 0x44, 0x61, 0x74, 0x61, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0x1e, 0x0a, 0x08, 0x54, 0x65, 0x78, 0x74, 0x44, 0x61, 0x74, 0x61, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x65, 0x78, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x65, 0x78, 0x74, 0x22, 0x44, 0x0a, 0x0a, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x48, 0x0a, 0x0b, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x12, 0x39, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x52, 0x0a, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x2a, 0x5e, 0x0a, 0x08, 0x53, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x12, 0x18, 0x0a, 0x14, 0x53, 0x45, 0x56, 0x45, 0x52, 0x49, 0x54, 0x59, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x4d, 0x49, 0x4e, 0x49, 0x4d, 0x41, 0x4c, 0x10, 0x01, 0x12, 0x07, 0x0a, 0x03, 0x4c, 0x4f, 0x57, 0x10, 0x02, 0x12, 0x0a, 0x0a, 0x06, 0x4d, 0x45, 0x44, 0x49, 0x55, 0x4d, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x48, 0x49, 0x47, 0x48, 0x10, 0x04, 0x12, 0x0c, 0x0a, 0x08, 0x43, 0x52, 0x49, 0x54, 0x49, 0x43, 0x41, 0x4c, 0x10, 0x05, 0x42, 0x74, 0x0a, 0x18, 0x63, 0x6f, 0x6d, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x42, 0x13, 0x56, 0x75, 0x6c, 0x6e, 0x65, 0x72, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x50, 0x01, 0x5a, 0x41, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2d, 0x73, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x2d, 0x73, 0x63, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x5f, 0x67, 0x6f, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, }) var ( file_vulnerability_proto_rawDescOnce sync.Once file_vulnerability_proto_rawDescData []byte ) func file_vulnerability_proto_rawDescGZIP() []byte { file_vulnerability_proto_rawDescOnce.Do(func() { file_vulnerability_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_vulnerability_proto_rawDesc), len(file_vulnerability_proto_rawDesc))) }) return file_vulnerability_proto_rawDescData } var file_vulnerability_proto_enumTypes = make([]protoimpl.EnumInfo, 1) var file_vulnerability_proto_msgTypes = make([]protoimpl.MessageInfo, 7) var file_vulnerability_proto_goTypes = []any{ (Severity)(0), // 0: tsunami.proto.Severity (*VulnerabilityId)(nil), // 1: tsunami.proto.VulnerabilityId (*Vulnerability)(nil), // 2: tsunami.proto.Vulnerability (*AdditionalDetail)(nil), // 3: tsunami.proto.AdditionalDetail (*BlobData)(nil), // 4: tsunami.proto.BlobData (*TextData)(nil), // 5: tsunami.proto.TextData (*Credential)(nil), // 6: tsunami.proto.Credential (*Credentials)(nil), // 7: tsunami.proto.Credentials } var file_vulnerability_proto_depIdxs = []int32{ 1, // 0: tsunami.proto.Vulnerability.main_id:type_name -> tsunami.proto.VulnerabilityId 1, // 1: tsunami.proto.Vulnerability.related_id:type_name -> tsunami.proto.VulnerabilityId 0, // 2: tsunami.proto.Vulnerability.severity:type_name -> tsunami.proto.Severity 3, // 3: tsunami.proto.Vulnerability.additional_details:type_name -> tsunami.proto.AdditionalDetail 4, // 4: tsunami.proto.AdditionalDetail.blob_data:type_name -> tsunami.proto.BlobData 5, // 5: tsunami.proto.AdditionalDetail.text_data:type_name -> tsunami.proto.TextData 6, // 6: tsunami.proto.AdditionalDetail.credential:type_name -> tsunami.proto.Credential 7, // 7: tsunami.proto.AdditionalDetail.credentials:type_name -> tsunami.proto.Credentials 6, // 8: tsunami.proto.Credentials.credential:type_name -> tsunami.proto.Credential 9, // [9:9] is the sub-list for method output_type 9, // [9:9] is the sub-list for method input_type 9, // [9:9] is the sub-list for extension type_name 9, // [9:9] is the sub-list for extension extendee 0, // [0:9] is the sub-list for field type_name } func init() { file_vulnerability_proto_init() } func file_vulnerability_proto_init() { if File_vulnerability_proto != nil { return } file_vulnerability_proto_msgTypes[2].OneofWrappers = []any{ (*AdditionalDetail_BlobData)(nil), (*AdditionalDetail_TextData)(nil), (*AdditionalDetail_Credential)(nil), (*AdditionalDetail_Credentials)(nil), } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_vulnerability_proto_rawDesc), len(file_vulnerability_proto_rawDesc)), NumEnums: 1, NumMessages: 7, NumExtensions: 0, NumServices: 0, }, GoTypes: file_vulnerability_proto_goTypes, DependencyIndexes: file_vulnerability_proto_depIdxs, EnumInfos: file_vulnerability_proto_enumTypes, MessageInfos: file_vulnerability_proto_msgTypes, }.Build() File_vulnerability_proto = out.File file_vulnerability_proto_goTypes = nil file_vulnerability_proto_depIdxs = nil } ================================================ FILE: proto/tsunami_go_proto/web_crawl.pb.go ================================================ // // Copyright 2020 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Data models for the web crawler. // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.5 // protoc v3.21.12 // source: web_crawl.proto package tsunami_go_proto import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" unsafe "unsafe" ) const ( // Verify that this generated code is sufficiently up-to-date. _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) // Verify that runtime/protoimpl is sufficiently up-to-date. _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) // Next ID: 7 type CrawlConfig struct { state protoimpl.MessageState `protogen:"open.v1"` // Starting points of a web crawl. // Required. SeedingUrls []string `protobuf:"bytes,1,rep,name=seeding_urls,json=seedingUrls,proto3" json:"seeding_urls,omitempty"` // The maximum depth of a web crawl. // Required. MaxDepth int32 `protobuf:"varint,2,opt,name=max_depth,json=maxDepth,proto3" json:"max_depth,omitempty"` // Allowed crawling scopes. // Optional. When empty, scopes are autogenerated from seeding_urls. Scopes []*CrawlConfig_Scope `protobuf:"bytes,3,rep,name=scopes,proto3" json:"scopes,omitempty"` // Whether crawling scope check should be enforced. // Optional. ShouldEnforceScopeCheck bool `protobuf:"varint,5,opt,name=should_enforce_scope_check,json=shouldEnforceScopeCheck,proto3" json:"should_enforce_scope_check,omitempty"` // The network endpoint to be crawled. // Required. NetworkEndpoint *NetworkEndpoint `protobuf:"bytes,6,opt,name=network_endpoint,json=networkEndpoint,proto3" json:"network_endpoint,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *CrawlConfig) Reset() { *x = CrawlConfig{} mi := &file_web_crawl_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *CrawlConfig) String() string { return protoimpl.X.MessageStringOf(x) } func (*CrawlConfig) ProtoMessage() {} func (x *CrawlConfig) ProtoReflect() protoreflect.Message { mi := &file_web_crawl_proto_msgTypes[0] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use CrawlConfig.ProtoReflect.Descriptor instead. func (*CrawlConfig) Descriptor() ([]byte, []int) { return file_web_crawl_proto_rawDescGZIP(), []int{0} } func (x *CrawlConfig) GetSeedingUrls() []string { if x != nil { return x.SeedingUrls } return nil } func (x *CrawlConfig) GetMaxDepth() int32 { if x != nil { return x.MaxDepth } return 0 } func (x *CrawlConfig) GetScopes() []*CrawlConfig_Scope { if x != nil { return x.Scopes } return nil } func (x *CrawlConfig) GetShouldEnforceScopeCheck() bool { if x != nil { return x.ShouldEnforceScopeCheck } return false } func (x *CrawlConfig) GetNetworkEndpoint() *NetworkEndpoint { if x != nil { return x.NetworkEndpoint } return nil } type CrawlTarget struct { state protoimpl.MessageState `protogen:"open.v1"` // The URL pointing to the document. Url string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"` // HTTP method to reach the url. Value must be in all upper case, like "GET". HttpMethod string `protobuf:"bytes,2,opt,name=http_method,json=httpMethod,proto3" json:"http_method,omitempty"` // An optional HTTP request body sent to the crawl URL. HttpRequestBody []byte `protobuf:"bytes,3,opt,name=http_request_body,json=httpRequestBody,proto3" json:"http_request_body,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *CrawlTarget) Reset() { *x = CrawlTarget{} mi := &file_web_crawl_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *CrawlTarget) String() string { return protoimpl.X.MessageStringOf(x) } func (*CrawlTarget) ProtoMessage() {} func (x *CrawlTarget) ProtoReflect() protoreflect.Message { mi := &file_web_crawl_proto_msgTypes[1] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use CrawlTarget.ProtoReflect.Descriptor instead. func (*CrawlTarget) Descriptor() ([]byte, []int) { return file_web_crawl_proto_rawDescGZIP(), []int{1} } func (x *CrawlTarget) GetUrl() string { if x != nil { return x.Url } return "" } func (x *CrawlTarget) GetHttpMethod() string { if x != nil { return x.HttpMethod } return "" } func (x *CrawlTarget) GetHttpRequestBody() []byte { if x != nil { return x.HttpRequestBody } return nil } // Represents an HTTP header. type HttpHeader struct { state protoimpl.MessageState `protogen:"open.v1"` Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *HttpHeader) Reset() { *x = HttpHeader{} mi := &file_web_crawl_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *HttpHeader) String() string { return protoimpl.X.MessageStringOf(x) } func (*HttpHeader) ProtoMessage() {} func (x *HttpHeader) ProtoReflect() protoreflect.Message { mi := &file_web_crawl_proto_msgTypes[2] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use HttpHeader.ProtoReflect.Descriptor instead. func (*HttpHeader) Descriptor() ([]byte, []int) { return file_web_crawl_proto_rawDescGZIP(), []int{2} } func (x *HttpHeader) GetKey() string { if x != nil { return x.Key } return "" } func (x *HttpHeader) GetValue() string { if x != nil { return x.Value } return "" } type CrawlResult struct { state protoimpl.MessageState `protogen:"open.v1"` // The target visited by the crawler. CrawlTarget *CrawlTarget `protobuf:"bytes,1,opt,name=crawl_target,json=crawlTarget,proto3" json:"crawl_target,omitempty"` // Depth at which the target was visited. CrawlDepth int32 `protobuf:"varint,2,opt,name=crawl_depth,json=crawlDepth,proto3" json:"crawl_depth,omitempty"` // Response code from the crawled target. ResponseCode int32 `protobuf:"varint,3,opt,name=response_code,json=responseCode,proto3" json:"response_code,omitempty"` // Content type of the resource served at the crawl target. ContentType string `protobuf:"bytes,4,opt,name=content_type,json=contentType,proto3" json:"content_type,omitempty"` // The content of the resource served at the crawl target. Content []byte `protobuf:"bytes,5,opt,name=content,proto3" json:"content,omitempty"` // Http headers of the response ResponseHeaders []*HttpHeader `protobuf:"bytes,6,rep,name=response_headers,json=responseHeaders,proto3" json:"response_headers,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *CrawlResult) Reset() { *x = CrawlResult{} mi := &file_web_crawl_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *CrawlResult) String() string { return protoimpl.X.MessageStringOf(x) } func (*CrawlResult) ProtoMessage() {} func (x *CrawlResult) ProtoReflect() protoreflect.Message { mi := &file_web_crawl_proto_msgTypes[3] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use CrawlResult.ProtoReflect.Descriptor instead. func (*CrawlResult) Descriptor() ([]byte, []int) { return file_web_crawl_proto_rawDescGZIP(), []int{3} } func (x *CrawlResult) GetCrawlTarget() *CrawlTarget { if x != nil { return x.CrawlTarget } return nil } func (x *CrawlResult) GetCrawlDepth() int32 { if x != nil { return x.CrawlDepth } return 0 } func (x *CrawlResult) GetResponseCode() int32 { if x != nil { return x.ResponseCode } return 0 } func (x *CrawlResult) GetContentType() string { if x != nil { return x.ContentType } return "" } func (x *CrawlResult) GetContent() []byte { if x != nil { return x.Content } return nil } func (x *CrawlResult) GetResponseHeaders() []*HttpHeader { if x != nil { return x.ResponseHeaders } return nil } // The crawler should only interact with web resources under certain scopes. type CrawlConfig_Scope struct { state protoimpl.MessageState `protogen:"open.v1"` // The domain of the scope, only URLs that are on the same domain or a // subdomain will be admitted for crawling. Domain might include a port. // Required. Domain string `protobuf:"bytes,1,opt,name=domain,proto3" json:"domain,omitempty"` // The path of the scope, only URLs that are under the same path will be // admitted for crawling. // Optional. When empty, all URLs under the same domain are allowed, // regardless of the paths. Path string `protobuf:"bytes,2,opt,name=path,proto3" json:"path,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *CrawlConfig_Scope) Reset() { *x = CrawlConfig_Scope{} mi := &file_web_crawl_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *CrawlConfig_Scope) String() string { return protoimpl.X.MessageStringOf(x) } func (*CrawlConfig_Scope) ProtoMessage() {} func (x *CrawlConfig_Scope) ProtoReflect() protoreflect.Message { mi := &file_web_crawl_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use CrawlConfig_Scope.ProtoReflect.Descriptor instead. func (*CrawlConfig_Scope) Descriptor() ([]byte, []int) { return file_web_crawl_proto_rawDescGZIP(), []int{0, 0} } func (x *CrawlConfig_Scope) GetDomain() string { if x != nil { return x.Domain } return "" } func (x *CrawlConfig_Scope) GetPath() string { if x != nil { return x.Path } return "" } var File_web_crawl_proto protoreflect.FileDescriptor var file_web_crawl_proto_rawDesc = string([]byte{ 0x0a, 0x0f, 0x77, 0x65, 0x62, 0x5f, 0x63, 0x72, 0x61, 0x77, 0x6c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0d, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x0d, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xca, 0x02, 0x0a, 0x0b, 0x43, 0x72, 0x61, 0x77, 0x6c, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x65, 0x65, 0x64, 0x69, 0x6e, 0x67, 0x5f, 0x75, 0x72, 0x6c, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x65, 0x65, 0x64, 0x69, 0x6e, 0x67, 0x55, 0x72, 0x6c, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x6d, 0x61, 0x78, 0x5f, 0x64, 0x65, 0x70, 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x6d, 0x61, 0x78, 0x44, 0x65, 0x70, 0x74, 0x68, 0x12, 0x38, 0x0a, 0x06, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x43, 0x72, 0x61, 0x77, 0x6c, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x52, 0x06, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x12, 0x3b, 0x0a, 0x1a, 0x73, 0x68, 0x6f, 0x75, 0x6c, 0x64, 0x5f, 0x65, 0x6e, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x5f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x5f, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x17, 0x73, 0x68, 0x6f, 0x75, 0x6c, 0x64, 0x45, 0x6e, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x12, 0x49, 0x0a, 0x10, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x5f, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x52, 0x0f, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x1a, 0x33, 0x0a, 0x05, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x4a, 0x04, 0x08, 0x04, 0x10, 0x05, 0x22, 0x6c, 0x0a, 0x0b, 0x43, 0x72, 0x61, 0x77, 0x6c, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x1f, 0x0a, 0x0b, 0x68, 0x74, 0x74, 0x70, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x68, 0x74, 0x74, 0x70, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x2a, 0x0a, 0x11, 0x68, 0x74, 0x74, 0x70, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x62, 0x6f, 0x64, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0f, 0x68, 0x74, 0x74, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x42, 0x6f, 0x64, 0x79, 0x22, 0x34, 0x0a, 0x0a, 0x48, 0x74, 0x74, 0x70, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x95, 0x02, 0x0a, 0x0b, 0x43, 0x72, 0x61, 0x77, 0x6c, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x3d, 0x0a, 0x0c, 0x63, 0x72, 0x61, 0x77, 0x6c, 0x5f, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x43, 0x72, 0x61, 0x77, 0x6c, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x52, 0x0b, 0x63, 0x72, 0x61, 0x77, 0x6c, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x63, 0x72, 0x61, 0x77, 0x6c, 0x5f, 0x64, 0x65, 0x70, 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0a, 0x63, 0x72, 0x61, 0x77, 0x6c, 0x44, 0x65, 0x70, 0x74, 0x68, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0c, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x12, 0x44, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x5f, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x48, 0x74, 0x74, 0x70, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x52, 0x0f, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x42, 0x6f, 0x0a, 0x18, 0x63, 0x6f, 0x6d, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x42, 0x0e, 0x57, 0x65, 0x62, 0x43, 0x72, 0x61, 0x77, 0x6c, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x50, 0x01, 0x5a, 0x41, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x2d, 0x73, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x2d, 0x73, 0x63, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x74, 0x73, 0x75, 0x6e, 0x61, 0x6d, 0x69, 0x5f, 0x67, 0x6f, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, }) var ( file_web_crawl_proto_rawDescOnce sync.Once file_web_crawl_proto_rawDescData []byte ) func file_web_crawl_proto_rawDescGZIP() []byte { file_web_crawl_proto_rawDescOnce.Do(func() { file_web_crawl_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_web_crawl_proto_rawDesc), len(file_web_crawl_proto_rawDesc))) }) return file_web_crawl_proto_rawDescData } var file_web_crawl_proto_msgTypes = make([]protoimpl.MessageInfo, 5) var file_web_crawl_proto_goTypes = []any{ (*CrawlConfig)(nil), // 0: tsunami.proto.CrawlConfig (*CrawlTarget)(nil), // 1: tsunami.proto.CrawlTarget (*HttpHeader)(nil), // 2: tsunami.proto.HttpHeader (*CrawlResult)(nil), // 3: tsunami.proto.CrawlResult (*CrawlConfig_Scope)(nil), // 4: tsunami.proto.CrawlConfig.Scope (*NetworkEndpoint)(nil), // 5: tsunami.proto.NetworkEndpoint } var file_web_crawl_proto_depIdxs = []int32{ 4, // 0: tsunami.proto.CrawlConfig.scopes:type_name -> tsunami.proto.CrawlConfig.Scope 5, // 1: tsunami.proto.CrawlConfig.network_endpoint:type_name -> tsunami.proto.NetworkEndpoint 1, // 2: tsunami.proto.CrawlResult.crawl_target:type_name -> tsunami.proto.CrawlTarget 2, // 3: tsunami.proto.CrawlResult.response_headers:type_name -> tsunami.proto.HttpHeader 4, // [4:4] is the sub-list for method output_type 4, // [4:4] is the sub-list for method input_type 4, // [4:4] is the sub-list for extension type_name 4, // [4:4] is the sub-list for extension extendee 0, // [0:4] is the sub-list for field type_name } func init() { file_web_crawl_proto_init() } func file_web_crawl_proto_init() { if File_web_crawl_proto != nil { return } file_network_proto_init() type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_web_crawl_proto_rawDesc), len(file_web_crawl_proto_rawDesc)), NumEnums: 0, NumMessages: 5, NumExtensions: 0, NumServices: 0, }, GoTypes: file_web_crawl_proto_goTypes, DependencyIndexes: file_web_crawl_proto_depIdxs, MessageInfos: file_web_crawl_proto_msgTypes, }.Build() File_web_crawl_proto = out.File file_web_crawl_proto_goTypes = nil file_web_crawl_proto_depIdxs = nil } ================================================ FILE: proto/vulnerability.proto ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ // Data models for describing a vulnerability. syntax = "proto3"; package tsunami.proto; option java_multiple_files = true; option java_outer_classname = "VulnerabilityProtos"; option java_package = "com.google.tsunami.proto"; option go_package = "github.com/google/tsunami-security-scanner/proto/go/vulnerability_go_proto"; // Severity of a vulnerability. enum Severity { // Unspecified severity. SEVERITY_UNSPECIFIED = 0; // Minimal severity. MINIMAL = 1; // Low severity. LOW = 2; // Medium severity. MEDIUM = 3; // High severity. HIGH = 4; // Critical severity. CRITICAL = 5; } // The identifier that uniquely identifies this vulnerability. message VulnerabilityId { // Entity that published this identifier. string publisher = 1; // Publisher assigned unique identifier. string value = 2; // Optional. URL for details about this vulnerability. string link = 3; } // Message that represents one single vulnerability detected by Tsunami. message Vulnerability { // The main identifier for this vulnerability, usually a publicly known // identifier like CVEs and such. If not publicly known, users are expected to // assign an id on their own. VulnerabilityId main_id = 1; // Any related identifiers about this vulnerability, e.g. a CWE weakness. repeated VulnerabilityId related_id = 2; // Severity of this vulnerability. Severity severity = 3; // Terse but descriptive sentence about this vulnerability. // For example: "Default Password (0p3nm35h) for 'root' Account.". string title = 4; // Verbose description of this vulnerability. string description = 5; // Optional. Verbose recommended solution(s). string recommendation = 6; // Optional. The CVSS v2 score of this vulnerability. string cvss_v2 = 7; // Optional. The CVSS v3 score of this vulnerability. string cvss_v3 = 8; // Any additional technical details about this vulnerability. repeated AdditionalDetail additional_details = 9; } // A list of vulnerabilities. Used mostly for export purposes. message VulnerabilityList { repeated Vulnerability vulnerabilities = 1; } // Additional details regarding a vulnerability can be stored here. Prefers to // use the existing structured data when possible, otherwise store the raw data // as a blob. message AdditionalDetail { string description = 1; oneof detail { BlobData blob_data = 2; TextData text_data = 3; Credential credential = 4; Credentials credentials = 5; } } // A piece of arbitrary binary data. message BlobData { bytes data = 1; } // A piece of arbitrary UTF-8 encoded text data. message TextData { string text = 1; } // Credential for a vulnerable network service. message Credential { string username = 1; string password = 2 ; } // A set of credentials for a vulnerable network service. message Credentials { repeated Credential credential = 1; } ================================================ FILE: proto/web_crawl.proto ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ // Data models for the web crawler. syntax = "proto3"; package tsunami.proto; import "network.proto"; option java_multiple_files = true; option java_outer_classname = "WebCrawlProtos"; option java_package = "com.google.tsunami.proto"; option go_package = "github.com/google/tsunami-security-scanner/proto/go/web_crawl_go_proto"; // Next ID: 7 message CrawlConfig { // The crawler should only interact with web resources under certain scopes. message Scope { // The domain of the scope, only URLs that are on the same domain or a // subdomain will be admitted for crawling. Domain might include a port. // Required. string domain = 1; // The path of the scope, only URLs that are under the same path will be // admitted for crawling. // Optional. When empty, all URLs under the same domain are allowed, // regardless of the paths. string path = 2; } // Starting points of a web crawl. // Required. repeated string seeding_urls = 1; // The maximum depth of a web crawl. // Required. int32 max_depth = 2; // Allowed crawling scopes. // Optional. When empty, scopes are autogenerated from seeding_urls. repeated Scope scopes = 3; // Whether crawling scope check should be enforced. // Optional. bool should_enforce_scope_check = 5; // The network endpoint to be crawled. // Required. NetworkEndpoint network_endpoint = 6; reserved 4; } message CrawlTarget { // The URL pointing to the document. string url = 1; // HTTP method to reach the url. Value must be in all upper case, like "GET". string http_method = 2; // An optional HTTP request body sent to the crawl URL. bytes http_request_body = 3; } // Represents an HTTP header. message HttpHeader { string key = 1; string value = 2; } // The type of content stored in the CrawlResult. enum CrawlContentType { CONTENT_TYPE_UNSPECIFIED = 0; CONTENT_TYPE_RAW = 1; CONTENT_TYPE_HASH = 2; } message CrawlResult { // The target visited by the crawler. CrawlTarget crawl_target = 1; // Depth at which the target was visited. int32 crawl_depth = 2; // Response code from the crawled target. int32 response_code = 3; // Content type of the resource served at the crawl target. string content_type = 4; // The content of the resource served at the crawl target. bytes content = 5; // Http headers of the response repeated HttpHeader response_headers = 6; // The type of content stored in the crawl_results. By default, the whole // response body is stored (RAW). But some configuration can request storing // only a hash of the response body (HASH). CrawlContentType crawl_content_type = 7; } ================================================ FILE: settings.gradle ================================================ pluginManagement { plugins { id 'com.github.johnrengelman.shadow' version '5.2.0' id 'com.google.protobuf' version '0.8.12' id 'net.ltgt.errorprone' version '1.1.1' } repositories { gradlePluginPortal() } } rootProject.name = 'tsunami' include ':tsunami-common' include ':tsunami-main' include ':tsunami-plugin' include ':tsunami-proto' include ':tsunami-workflow' project(':tsunami-common').projectDir = "$rootDir/common" as File project(':tsunami-main').projectDir = "$rootDir/main" as File project(':tsunami-plugin').projectDir = "$rootDir/plugin" as File project(':tsunami-proto').projectDir = "$rootDir/proto" as File project(':tsunami-workflow').projectDir = "$rootDir/workflow" as File def tcsRepository = System.getenv("GITREPO_TSUNAMI_TCS") ?: "https://github.com/google/tsunami-security-scanner-callback-server.git" sourceControl { gitRepository("${tcsRepository}") { producesModule("com.google.tsunami:tcs-common") producesModule("com.google.tsunami:tcs-proto") } } ================================================ FILE: tsunami.yaml ================================================ # This is an example YAML config file for Tsunami security scanner. # TODO: add examples for all available configuration options. # Socket factory configuration for TCP connections. # These settings ensure all sockets created by plugins have proper timeouts, # preventing plugins from hanging indefinitely when servers don't respond. common: net: socket: # Timeout in seconds for establishing TCP connections (default: 10) connect_timeout_seconds: 10 # Timeout in seconds for read operations on sockets (default: 30) read_timeout_seconds: 30 # Whether to trust all SSL certificates (default: true) trust_all_certificates: true ================================================ FILE: tsunami_tcs.yaml ================================================ plugin: callbackserver: callback_address: "127.0.0.1" # Running callback server locally callback_port: 8881 # Make sure to match with ones configured in tcs_config.yaml polling_uri: "http://127.0.0.1:8880" common: net: http: trust_all_certificates: true connect_timeout_seconds: 60 ================================================ FILE: workflow/README.md ================================================ # Tsunami Workflow Module ## Overview This module defines basic workflows for network scanning. ================================================ FILE: workflow/build.gradle ================================================ description = 'Tsunami: Workflow' dependencies { implementation project(':tsunami-common') implementation project(':tsunami-plugin') implementation project(':tsunami-proto') implementation "com.google.flogger:flogger:0.9" implementation "com.google.flogger:google-extensions:0.9" implementation "com.google.guava:guava:33.0.0-jre" implementation "com.google.protobuf:protobuf-java-util:3.25.5" implementation "com.google.protobuf:protobuf-java:3.25.5" implementation "javax.inject:javax.inject:1" testImplementation "com.google.guava:guava-testlib:33.0.0-jre" testImplementation "com.google.truth:truth:1.4.4" testImplementation "com.google.truth.extensions:truth-java8-extension:1.4.4" testImplementation "com.google.truth.extensions:truth-proto-extension:1.4.4" testImplementation "junit:junit:4.13.2" } tasks.named("javadoc") { dependsOn(":tsunami-plugin:shadowJar") dependsOn(":tsunami-proto:shadowJar") } tasks.named("shadowJar") { dependsOn(":tsunami-proto:shadowJar") dependsOn(":tsunami-plugin:shadowJar") } tasks.named("compileTestJava") { dependsOn(":tsunami-plugin:shadowJar") dependsOn(":tsunami-proto:shadowJar") } tasks.named("compileJava") { dependsOn(":tsunami-plugin:shadowJar") } tasks.named('compileJava') { dependsOn(':tsunami-proto:shadowJar') } ================================================ FILE: workflow/src/main/java/com/google/tsunami/workflow/AdvisoriesWorkflow.java ================================================ /* * Copyright 2025 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.workflow; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.collect.ImmutableList.toImmutableList; import static java.nio.charset.StandardCharsets.UTF_8; import com.google.common.flogger.GoogleLogger; import com.google.protobuf.TextFormat; import com.google.tsunami.plugin.PluginManager; import com.google.tsunami.proto.VulnerabilityList; import java.io.IOException; import java.io.PrintWriter; import javax.inject.Inject; /** Workflow for dumping advisories. */ public final class AdvisoriesWorkflow { private static final GoogleLogger logger = GoogleLogger.forEnclosingClass(); private final PluginManager pluginManager; @Inject AdvisoriesWorkflow(PluginManager pluginManager) { this.pluginManager = checkNotNull(pluginManager); } public Void run(String path) { logger.atInfo().log("Dumping advisories to %s", path); var vulnerabilityList = buildVulnerabilityList(); var vulnerabilityText = TextFormat.printer().printToString(vulnerabilityList); try (PrintWriter writer = new PrintWriter(path, UTF_8.name())) { writer.write(vulnerabilityText); } catch (IOException e) { logger.atSevere().withCause(e).log("Failed to dump advisories to %s", path); } return null; } private VulnerabilityList buildVulnerabilityList() { var vulnerabilities = pluginManager.getAllVulnDetectors().stream() .flatMap(plugin -> plugin.getAdvisories().stream()) .collect(toImmutableList()); return VulnerabilityList.newBuilder().addAllVulnerabilities(vulnerabilities).build(); } } ================================================ FILE: workflow/src/main/java/com/google/tsunami/workflow/DefaultScanningWorkflow.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.workflow; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.util.concurrent.Futures.immediateFailedFuture; import static com.google.common.util.concurrent.Futures.immediateFuture; import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import com.google.common.base.Joiner; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import com.google.common.flogger.GoogleLogger; import com.google.common.util.concurrent.FluentFuture; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.protobuf.util.Durations; import com.google.protobuf.util.Timestamps; import com.google.tsunami.common.TsunamiException; import com.google.tsunami.common.time.UtcClock; import com.google.tsunami.plugin.LanguageServerException; import com.google.tsunami.plugin.PluginExecutionException; import com.google.tsunami.plugin.PluginExecutionResult; import com.google.tsunami.plugin.PluginExecutor; import com.google.tsunami.plugin.PluginExecutor.PluginExecutorConfig; import com.google.tsunami.plugin.PluginManager; import com.google.tsunami.plugin.PluginManager.PluginMatchingResult; import com.google.tsunami.plugin.PortScanner; import com.google.tsunami.plugin.ServiceFingerprinter; import com.google.tsunami.plugin.VulnDetector; import com.google.tsunami.proto.DetectionReport; import com.google.tsunami.proto.DetectionReportList; import com.google.tsunami.proto.DetectionStatus; import com.google.tsunami.proto.FingerprintingReport; import com.google.tsunami.proto.FullDetectionReports; import com.google.tsunami.proto.NetworkService; import com.google.tsunami.proto.PortScanningReport; import com.google.tsunami.proto.ReconnaissanceReport; import com.google.tsunami.proto.ScanFinding; import com.google.tsunami.proto.ScanResults; import com.google.tsunami.proto.ScanStatus; import com.google.tsunami.proto.ScanTarget; import com.google.tsunami.proto.TargetInfo; import java.time.Clock; import java.time.Duration; import java.time.Instant; import java.util.Collection; import java.util.List; import java.util.Optional; import java.util.concurrent.ExecutionException; import javax.inject.Inject; import javax.inject.Provider; /** * Default scanning workflow for Tsunami. * *

This workflow is intended to be invoked by Tsunami's command line tool. One {@link * DefaultScanningWorkflow} object will be created for one target IP or hostname, and scans for * different IP / hostname target will be executed in isolated processes. * *

Tsunami performs the network scanning in the following steps: * *

    *
  1. Port scanning using NMap. *
  2. If target serves any web application, web fingerprinting to determine the exposed * application. *
  3. Vulnerability detection by matching the identified network services to corresponding * detectors. *
*/ // TODO(b/145315535): provide a cleaner API to avoid executing the same workflow on different scan // target. public final class DefaultScanningWorkflow { private static final GoogleLogger logger = GoogleLogger.forEnclosingClass(); private final PluginManager pluginManager; private final Clock clock; private final Provider pluginExecutorProvider; private Instant scanStartTimestamp; private ExecutionTracer executionTracer; @Inject public DefaultScanningWorkflow( PluginManager pluginManager, @UtcClock Clock clock, Provider pluginExecutorProvider) { this.pluginManager = checkNotNull(pluginManager); this.clock = checkNotNull(clock); this.pluginExecutorProvider = checkNotNull(pluginExecutorProvider); } public ExecutionTracer getExecutionTracer() { return executionTracer; } /** * Performs the scanning workflow in blocking manner. * * @param scanTarget the IP or hostname target to be scanned * @return The result of the scanning workflow. * @throws ExecutionException if the scanning workflow execution thread failed. * @throws InterruptedException if interrupted during scanning workflow execution. */ public ScanResults run(ScanTarget scanTarget) throws ExecutionException, InterruptedException { return runAsync(scanTarget).get(); } /** * Performs the scanning workflow asynchronously. * * @param scanTarget the IP or hostname or uri target to be scanned * @return A {@link ListenableFuture} over the result of the scanning workflow. */ public ListenableFuture runAsync(ScanTarget scanTarget) { checkNotNull(scanTarget); scanStartTimestamp = Instant.now(clock); executionTracer = ExecutionTracer.startWorkflow(); logger.atInfo().log("Staring Tsunami scanning workflow."); FluentFuture reconnaissanceReport; if (scanTarget.hasNetworkService()) { PortScanningReport portScanningReport = buildUriPortScanningReport(scanTarget); reconnaissanceReport = FluentFuture.from(fingerprintNetworkServices(portScanningReport)); } else { reconnaissanceReport = FluentFuture.from(scanPorts(scanTarget)) .transformAsync(this::fingerprintNetworkServices, directExecutor()); } return reconnaissanceReport .transformAsync(this::detectVulnerabilities, directExecutor()) // Unfortunately FluentFuture doesn't support future peeking. .transform( scanResults -> { logger.atInfo().log("%s", executionTracer.buildLoggableExecutionTrace(scanResults)); return scanResults; }, directExecutor()) // Execution errors are handled and reported back in the ScanResults. .catching(PluginExecutionException.class, this::onExecutionError, directExecutor()) .catching(LanguageServerException.class, this::onExecutionError, directExecutor()) .catching(ScanningWorkflowException.class, this::onExecutionError, directExecutor()); } private PortScanningReport buildUriPortScanningReport(ScanTarget scanTarget) { Optional> matchedPortScanner = pluginManager.getPortScanner(); executionTracer.startPortScanning(ImmutableList.of(matchedPortScanner.get())); NetworkService networkService = scanTarget.getNetworkService(); return PortScanningReport.newBuilder() .setTargetInfo( TargetInfo.newBuilder().addNetworkEndpoints(networkService.getNetworkEndpoint())) .addNetworkServices(networkService) .build(); } private ScanResults onExecutionError(TsunamiException exception) { logger.atSevere().withCause(exception).log("Tsunami scan failed, aborting workflow!!!"); return buildScanResultForFailure(exception); } private ListenableFuture scanPorts(ScanTarget scanTarget) throws ScanningWorkflowException { Optional> matchedPortScanner = pluginManager.getPortScanner(); if (!matchedPortScanner.isPresent()) { return immediateFailedFuture( new ScanningWorkflowException("At least one PortScanner plugin is required")); } PluginExecutorConfig executorConfig = PluginExecutorConfig.builder() .setMatchedPlugin(matchedPortScanner.get()) .setPluginExecutionLogic( () -> matchedPortScanner.get().tsunamiPlugin().scan(scanTarget)) .build(); executionTracer.startPortScanning(ImmutableList.of(matchedPortScanner.get())); logger.atInfo().log("Starting port scanning phase of the scanning workflow."); return FluentFuture.from(pluginExecutorProvider.get().executeAsync(executorConfig)) .transformAsync( pluginExecutionResult -> pluginExecutionResult.isSucceeded() ? immediateFuture(pluginExecutionResult.resultData().get()) : immediateFailedFuture(pluginExecutionResult.exception().get()), directExecutor()); } private ListenableFuture fingerprintNetworkServices( PortScanningReport portScanningReport) { checkNotNull(portScanningReport); // For each network service, find matching fingerprinting plugin, otherwise directly add to // ReconnaissanceReport. TargetInfo targetInfo = portScanningReport.getTargetInfo(); List> matchedFingerprinters = Lists.newArrayList(); List networkServicesToKeep = Lists.newArrayList(); for (NetworkService networkService : portScanningReport.getNetworkServicesList()) { Optional> matchedFingerprinter = pluginManager.getServiceFingerprinter(networkService); if (matchedFingerprinter.isPresent()) { matchedFingerprinters.add(matchedFingerprinter.get()); } else { networkServicesToKeep.add(networkService); } } executionTracer.startServiceFingerprinting(ImmutableList.copyOf(matchedFingerprinters)); logger.atInfo().log( "Port scanning phase done, moving to service fingerprinting phase with '%d'" + " fingerprinter(s) selected.", matchedFingerprinters.size()); // Execute matched fingerprinters asynchronously. ImmutableList>> fingerprintingResultFutures = matchedFingerprinters.stream() .map(fingerprinter -> buildFingerprinterExecutorConfig(targetInfo, fingerprinter)) .map(executorConfig -> pluginExecutorProvider.get().executeAsync(executorConfig)) .collect(toImmutableList()); return FluentFuture.from(Futures.successfulAsList(fingerprintingResultFutures)) .transform( executionResults -> ReconnaissanceReport.newBuilder() .setTargetInfo(targetInfo) .addAllNetworkServices(networkServicesToKeep) .addAllNetworkServices(getFingerprintedServices(executionResults)) .build(), directExecutor()); } private static PluginExecutorConfig buildFingerprinterExecutorConfig( TargetInfo targetInfo, PluginMatchingResult fingerprinter) { return PluginExecutorConfig.builder() .setMatchedPlugin(fingerprinter) .setPluginExecutionLogic( () -> fingerprinter .tsunamiPlugin() .fingerprint(targetInfo, fingerprinter.matchedServices().get(0))) .build(); } @SuppressWarnings("unchecked") private static ImmutableList getFingerprintedServices( Collection> executionResults) { return executionResults.stream() .flatMap( result -> result.isSucceeded() ? result.resultData().get().getNetworkServicesList().stream() : ((List) result.executorConfig().matchedPlugin().matchedServices()) .stream()) .collect(toImmutableList()); } private ListenableFuture detectVulnerabilities( ReconnaissanceReport reconnaissanceReport) { checkNotNull(reconnaissanceReport); ImmutableList> matchedVulnDetectors = pluginManager.getVulnDetectors(reconnaissanceReport); executionTracer.startVulnerabilityDetecting(matchedVulnDetectors); logger.atInfo().log("Service fingerprinting phase done, moving to vuln detection phase."); ImmutableList>> detectionResultFutures = matchedVulnDetectors.stream() .map( matchedVulnDetector -> PluginExecutorConfig.builder() .setMatchedPlugin(matchedVulnDetector) .setPluginExecutionLogic( () -> matchedVulnDetector .tsunamiPlugin() .detect( reconnaissanceReport.getTargetInfo(), matchedVulnDetector.matchedServices())) .build()) .map( vulnDetectorExecutorConfig -> pluginExecutorProvider.get().executeAsync(vulnDetectorExecutorConfig)) .collect(toImmutableList()); return FluentFuture.from(Futures.successfulAsList(detectionResultFutures)) .transform( detectionResult -> generateScanResults(detectionResult, reconnaissanceReport), directExecutor()); } private ScanResults generateScanResults( Collection> detectionResults, ReconnaissanceReport reconnaissanceReport) { executionTracer.setDone(); logger.atInfo().log("Tsunami scanning workflow done. Generating scan results."); ImmutableList succeededDetectionReports = detectionResults.stream() .filter(PluginExecutionResult::isSucceeded) .flatMap( detectionResult -> detectionResult.resultData().get().getDetectionReportsList().stream()) .collect(toImmutableList()); ImmutableList failedPlugins = detectionResults.stream() .filter(executionResult -> !executionResult.isSucceeded()) .map(executionResult -> executionResult.executorConfig().matchedPlugin().pluginId()) .collect(toImmutableList()); ScanStatus scanStatus; String statusMessage = ""; if (failedPlugins.isEmpty()) { scanStatus = ScanStatus.SUCCEEDED; } else if (failedPlugins.size() == detectionResults.size()) { scanStatus = ScanStatus.FAILED; statusMessage = "All VulnDetectors failed."; } else { scanStatus = ScanStatus.PARTIALLY_SUCCEEDED; statusMessage = "Failed plugins:\n" + Joiner.on("\n").join(failedPlugins); } boolean targetAlive = false; if (reconnaissanceReport.getNetworkServicesCount() > 0 || !detectionResults.isEmpty()) { targetAlive = true; } return ScanResults.newBuilder() .setScanStatus(scanStatus) .setStatusMessage(statusMessage) .setTargetAlive(targetAlive) .addAllScanFindings( succeededDetectionReports.stream() .filter( detectionReport -> detectionReport .getDetectionStatus() .equals(DetectionStatus.VULNERABILITY_VERIFIED) || detectionReport .getDetectionStatus() .equals(DetectionStatus.VULNERABILITY_PRESENT)) .map( detectionReport -> ScanFinding.newBuilder() .setTargetInfo(detectionReport.getTargetInfo()) .setNetworkService(detectionReport.getNetworkService()) .setVulnerability(detectionReport.getVulnerability()) .build()) .collect(toImmutableList())) .setScanStartTimestamp(Timestamps.fromMillis(scanStartTimestamp.toEpochMilli())) .setScanDuration( Durations.fromMillis( Duration.between(scanStartTimestamp, Instant.now(clock)).toMillis())) .setFullDetectionReports( FullDetectionReports.newBuilder().addAllDetectionReports(succeededDetectionReports)) .setReconnaissanceReport(reconnaissanceReport) .build(); } private ScanResults buildScanResultForFailure(TsunamiException exception) { executionTracer.forceDone(); return ScanResults.newBuilder() .setScanStatus(ScanStatus.FAILED) .setStatusMessage(exception.getMessage()) .setScanStartTimestamp(Timestamps.fromMillis(scanStartTimestamp.toEpochMilli())) .setScanDuration( Durations.fromMillis( Duration.between(scanStartTimestamp, Instant.now(clock)).toMillis())) .build(); } } ================================================ FILE: workflow/src/main/java/com/google/tsunami/workflow/ExecutionStage.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.workflow; /** Defines the scan execution stages. */ public enum ExecutionStage { START, PORT_SCANNING, SERVICE_FINGERPRINTING, VULNERABILITY_DETECTING, DONE } ================================================ FILE: workflow/src/main/java/com/google/tsunami/workflow/ExecutionTracer.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.workflow; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; import static com.google.common.collect.ImmutableList.toImmutableList; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Joiner; import com.google.common.base.Stopwatch; import com.google.common.collect.ImmutableList; import com.google.tsunami.plugin.PluginManager.PluginMatchingResult; import com.google.tsunami.plugin.PortScanner; import com.google.tsunami.plugin.ServiceFingerprinter; import com.google.tsunami.plugin.TsunamiPlugin; import com.google.tsunami.plugin.VulnDetector; import com.google.tsunami.proto.NetworkService; import com.google.tsunami.proto.ScanResults; import java.time.Duration; /** Traces the execution of the default scanning workflow. */ final class ExecutionTracer { private static final Joiner PLUGIN_INFO_JOINER = Joiner.on("\n "); private static final Joiner NETWORK_SERVICE_JOINER = Joiner.on(", "); private final Stopwatch portScanningTimer; private final Stopwatch serviceFingerprintingTimer; private final Stopwatch vulnerabilityDetectingTimer; private ExecutionStage currentExecutionStage; private ImmutableList> selectedPortScanners; private ImmutableList> selectedServiceFingerprinters; private ImmutableList> selectedVulnDetectors; private ExecutionTracer() { this(Stopwatch.createUnstarted(), Stopwatch.createUnstarted(), Stopwatch.createUnstarted()); } @VisibleForTesting ExecutionTracer( Stopwatch portScanningTimer, Stopwatch serviceFingerprintingTimer, Stopwatch vulnerabilityDetectingTimer) { this.currentExecutionStage = ExecutionStage.START; this.portScanningTimer = checkNotNull(portScanningTimer); this.serviceFingerprintingTimer = checkNotNull(serviceFingerprintingTimer); this.vulnerabilityDetectingTimer = checkNotNull(vulnerabilityDetectingTimer); } static ExecutionTracer startWorkflow() { return new ExecutionTracer(); } ExecutionStage getCurrentExecutionStage() { return this.currentExecutionStage; } void startPortScanning(ImmutableList> selectedPortScanners) { checkState(currentExecutionStage.equals(ExecutionStage.START)); this.portScanningTimer.start(); this.currentExecutionStage = ExecutionStage.PORT_SCANNING; this.selectedPortScanners = checkNotNull(selectedPortScanners); } void startServiceFingerprinting( ImmutableList> selectedServiceFingerprinters) { checkState(currentExecutionStage.equals(ExecutionStage.PORT_SCANNING)); checkState(portScanningTimer.isRunning()); this.portScanningTimer.stop(); this.serviceFingerprintingTimer.start(); this.currentExecutionStage = ExecutionStage.SERVICE_FINGERPRINTING; this.selectedServiceFingerprinters = checkNotNull(selectedServiceFingerprinters); } void startVulnerabilityDetecting( ImmutableList> selectedVulnDetectors) { checkState(currentExecutionStage.equals(ExecutionStage.SERVICE_FINGERPRINTING)); checkState(serviceFingerprintingTimer.isRunning()); this.serviceFingerprintingTimer.stop(); this.vulnerabilityDetectingTimer.start(); this.currentExecutionStage = ExecutionStage.VULNERABILITY_DETECTING; this.selectedVulnDetectors = checkNotNull(selectedVulnDetectors); } void setDone() { checkState(currentExecutionStage.equals(ExecutionStage.VULNERABILITY_DETECTING)); checkState(!portScanningTimer.isRunning()); checkState(!serviceFingerprintingTimer.isRunning()); checkState(vulnerabilityDetectingTimer.isRunning()); this.vulnerabilityDetectingTimer.stop(); this.currentExecutionStage = ExecutionStage.DONE; } void forceDone() { if (portScanningTimer.isRunning()) { portScanningTimer.stop(); } if (serviceFingerprintingTimer.isRunning()) { serviceFingerprintingTimer.stop(); } if (vulnerabilityDetectingTimer.isRunning()) { vulnerabilityDetectingTimer.stop(); } this.currentExecutionStage = ExecutionStage.DONE; } boolean isDone() { return currentExecutionStage.equals(ExecutionStage.DONE); } Duration getPortScanningStageRuntime() { return portScanningTimer.elapsed(); } Duration getServiceFingerprintingStageRuntime() { return serviceFingerprintingTimer.elapsed(); } Duration getVulnerabilityDetectingStageRuntime() { return vulnerabilityDetectingTimer.elapsed(); } ImmutableList> getSelectedPortScanners() { return selectedPortScanners; } ImmutableList> getSelectedServiceFingerprinters() { return selectedServiceFingerprinters; } ImmutableList> getSelectedVulnDetectors() { return selectedVulnDetectors; } String buildLoggableExecutionTrace(ScanResults scanResults) { checkState(isDone()); return new StringBuilder("Tsunami scanning workflow traces:\n") // Port scanning phase. .append( String.format( " Port scanning phase (%s) with %d plugin(s):\n ", portScanningTimer, selectedPortScanners.size())) .append( PLUGIN_INFO_JOINER.join( selectedPortScanners.stream() .map(ExecutionTracer::buildPluginInfoMessage) .collect(toImmutableList()))) // Service fingerprinting phase. .append( String.format( "\n Service fingerprinting phase (%s) with %d plugin(s):\n ", serviceFingerprintingTimer, selectedServiceFingerprinters.size())) .append( PLUGIN_INFO_JOINER.join( selectedServiceFingerprinters.stream() .map(ExecutionTracer::buildPluginInfoMessage) .collect(toImmutableList()))) // Vuln detection phase. .append( String.format( "\n Vuln detection phase (%s) with %d plugin(s):\n ", vulnerabilityDetectingTimer, selectedVulnDetectors.size())) .append( PLUGIN_INFO_JOINER.join( selectedVulnDetectors.stream() .map(ExecutionTracer::buildPluginInfoMessage) .collect(toImmutableList()))) .append( String.format( "\n # of detected vulnerability: %d.", scanResults.getScanFindingsCount())) .toString(); } static String buildPluginInfoMessage( PluginMatchingResult pluginMatchingResult) { // TODO(b/145315535): add execution time once plugin execution logic is moved to a dedicated // plugin executor. StringBuilder pluginInfoBuilder = new StringBuilder(pluginMatchingResult.pluginId()); if (!pluginMatchingResult.matchedServices().isEmpty()) { pluginInfoBuilder .append(" was selected for the following services: ") .append( NETWORK_SERVICE_JOINER.join( pluginMatchingResult.matchedServices().stream() .map(ExecutionTracer::formatNetworkService) .collect(toImmutableList()))); } return pluginInfoBuilder.toString(); } static String formatNetworkService(NetworkService networkService) { return String.format( "%s (%s, port %d)", networkService.getServiceName(), networkService.getTransportProtocol(), networkService.getNetworkEndpoint().getPort().getPortNumber()); } } ================================================ FILE: workflow/src/main/java/com/google/tsunami/workflow/ScanningWorkflowException.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.workflow; import com.google.tsunami.common.ErrorCode; import com.google.tsunami.common.TsunamiException; /** Signals a well known and recognized error occurred while executing the scanning workflow. */ public class ScanningWorkflowException extends TsunamiException { public ScanningWorkflowException(String message) { super(ErrorCode.WORKFLOW_ERROR, message); } } ================================================ FILE: workflow/src/test/java/com/google/tsunami/workflow/DefaultScanningWorkflowTest.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.workflow; import static com.google.common.truth.Truth.assertThat; import static com.google.tsunami.common.data.NetworkEndpointUtils.forIp; import static com.google.tsunami.common.data.NetworkServiceUtils.buildUriNetworkService; import static org.junit.Assert.assertThrows; import com.google.common.collect.ImmutableList; import com.google.inject.Guice; import com.google.inject.Injector; import com.google.tsunami.common.net.http.HttpClientModule; import com.google.tsunami.common.time.testing.FakeUtcClockModule; import com.google.tsunami.plugin.payload.PayloadGeneratorModule; import com.google.tsunami.plugin.testing.FailedPortScannerBootstrapModule; import com.google.tsunami.plugin.testing.FailedRemoteVulnDetectorBootstrapModule; import com.google.tsunami.plugin.testing.FailedServiceFingerprinterBootstrapModule; import com.google.tsunami.plugin.testing.FailedVulnDetectorBootstrapModule; import com.google.tsunami.plugin.testing.FakePluginExecutionModule; import com.google.tsunami.plugin.testing.FakePortScanner; import com.google.tsunami.plugin.testing.FakePortScannerBootstrapModule; import com.google.tsunami.plugin.testing.FakePortScannerBootstrapModule2; import com.google.tsunami.plugin.testing.FakePortScannerBootstrapModuleEmpty; import com.google.tsunami.plugin.testing.FakeRemoteVulnDetector; import com.google.tsunami.plugin.testing.FakeRemoteVulnDetectorBootstrapModule; import com.google.tsunami.plugin.testing.FakeServiceFingerprinter; import com.google.tsunami.plugin.testing.FakeServiceFingerprinterBootstrapModule; import com.google.tsunami.plugin.testing.FakeVulnDetector; import com.google.tsunami.plugin.testing.FakeVulnDetector2; import com.google.tsunami.plugin.testing.FakeVulnDetectorBootstrapModule; import com.google.tsunami.plugin.testing.FakeVulnDetectorBootstrapModule2; import com.google.tsunami.plugin.testing.FakeVulnDetectorBootstrapModuleEmpty; import com.google.tsunami.proto.ScanResults; import com.google.tsunami.proto.ScanStatus; import com.google.tsunami.proto.ScanTarget; import java.security.SecureRandom; import java.util.concurrent.ExecutionException; import javax.inject.Inject; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** Tests for {@link DefaultScanningWorkflow}. */ @RunWith(JUnit4.class) public final class DefaultScanningWorkflowTest { @Inject private DefaultScanningWorkflow scanningWorkflow; @Before public void setUp() { Guice.createInjector( new HttpClientModule.Builder().build(), new PayloadGeneratorModule(new SecureRandom()), new FakeUtcClockModule(), new FakePluginExecutionModule(), new FakePortScannerBootstrapModule(), new FakePortScannerBootstrapModule2(), new FakeServiceFingerprinterBootstrapModule(), new FakeVulnDetectorBootstrapModule(), new FakeVulnDetectorBootstrapModule2(), new FakeRemoteVulnDetectorBootstrapModule()) .injectMembers(this); } @Test public void run_whenAllRequiredPluginsInstalled_executesScanningWorkflow() throws InterruptedException, ExecutionException { ScanResults scanResults = scanningWorkflow.run(buildScanTarget()); ExecutionTracer executionTracer = scanningWorkflow.getExecutionTracer(); assertThat(scanResults.getScanStatus()).isEqualTo(ScanStatus.SUCCEEDED); assertThat(executionTracer.isDone()).isTrue(); assertThat(executionTracer.getSelectedPortScanners()).hasSize(1); assertThat(executionTracer.getSelectedPortScanners().get(0).tsunamiPlugin().getClass()) .isEqualTo(FakePortScanner.class); assertThat(executionTracer.getSelectedServiceFingerprinters()).hasSize(1); assertThat(executionTracer.getSelectedServiceFingerprinters().get(0).tsunamiPlugin().getClass()) .isEqualTo(FakeServiceFingerprinter.class); assertThat( executionTracer.getSelectedVulnDetectors().stream() .map(selectedVulnDetector -> selectedVulnDetector.tsunamiPlugin().getClass())) .containsExactlyElementsIn( ImmutableList.of( FakeVulnDetector.class, FakeVulnDetector2.class, FakeRemoteVulnDetector.class)); } @Test public void run_whenUriScanTarget_executesScanningWorkflow() throws InterruptedException, ExecutionException { ScanResults scanResults = scanningWorkflow.run(buildUriScanTarget()); ExecutionTracer executionTracer = scanningWorkflow.getExecutionTracer(); assertThat(scanResults.getScanStatus()).isEqualTo(ScanStatus.SUCCEEDED); assertThat(executionTracer.isDone()).isTrue(); assertThat( executionTracer.getSelectedVulnDetectors().stream() .map(selectedVulnDetector -> selectedVulnDetector.tsunamiPlugin().getClass())) .containsExactlyElementsIn( ImmutableList.of( FakeVulnDetector.class, FakeVulnDetector2.class, FakeRemoteVulnDetector.class)); assertThat(scanResults.getScanFindings(0).getNetworkService().getServiceName()) .isEqualTo("https"); assertThat( scanResults .getScanFindings(0) .getNetworkService() .getServiceContext() .getWebServiceContext() .getApplicationRoot()) .isEqualTo("/function1"); assertThat( scanResults .getScanFindings(0) .getNetworkService() .getNetworkEndpoint() .getPort() .getPortNumber()) .isEqualTo(443); assertThat( scanResults .getScanFindings(0) .getNetworkService() .getNetworkEndpoint() .getHostname() .getName()) .isEqualTo("localhost"); } // TODO(b/145315535): add default output for the fake plugins and test the output of the workflow. @Test public void run_whenNoPortScannerInstalled_returnsFailedScanResult() throws ExecutionException, InterruptedException { Injector injector = Guice.createInjector( new HttpClientModule.Builder().build(), new PayloadGeneratorModule(new SecureRandom()), new FakeUtcClockModule(), new FakePluginExecutionModule(), new FakeServiceFingerprinterBootstrapModule(), new FakeVulnDetectorBootstrapModule()); scanningWorkflow = injector.getInstance(DefaultScanningWorkflow.class); ScanResults scanResults = scanningWorkflow.run(buildScanTarget()); assertThat(scanResults.getScanStatus()).isEqualTo(ScanStatus.FAILED); assertThat(scanResults.getStatusMessage()) .contains("At least one PortScanner plugin is required"); assertThat(scanResults.getScanFindingsList()).isEmpty(); } @Test public void run_whenNoFingerprinterInstalled_executesScanningWorkflow() throws InterruptedException, ExecutionException { Injector injector = Guice.createInjector( new HttpClientModule.Builder().build(), new PayloadGeneratorModule(new SecureRandom()), new FakeUtcClockModule(), new FakePluginExecutionModule(), new FakePortScannerBootstrapModule(), new FakeVulnDetectorBootstrapModule(), new FakeVulnDetectorBootstrapModule2()); scanningWorkflow = injector.getInstance(DefaultScanningWorkflow.class); ScanResults scanResults = scanningWorkflow.run(buildScanTarget()); ExecutionTracer executionTracer = scanningWorkflow.getExecutionTracer(); assertThat(scanResults.getScanStatus()).isEqualTo(ScanStatus.SUCCEEDED); assertThat(executionTracer.isDone()).isTrue(); assertThat(executionTracer.getSelectedPortScanners()).hasSize(1); assertThat(executionTracer.getSelectedPortScanners().get(0).tsunamiPlugin().getClass()) .isEqualTo(FakePortScanner.class); assertThat(executionTracer.getSelectedServiceFingerprinters()).isEmpty(); assertThat( executionTracer.getSelectedVulnDetectors().stream() .map(selectedVulnDetector -> selectedVulnDetector.tsunamiPlugin().getClass())) .containsExactlyElementsIn( ImmutableList.of(FakeVulnDetector.class, FakeVulnDetector2.class)); } @Test public void run_whenPortScannerFailed_returnsFailedScanResult() throws ExecutionException, InterruptedException { Injector injector = Guice.createInjector( new HttpClientModule.Builder().build(), new PayloadGeneratorModule(new SecureRandom()), new FakeUtcClockModule(), new FakePluginExecutionModule(), new FailedPortScannerBootstrapModule(), new FakeServiceFingerprinterBootstrapModule(), new FakeVulnDetectorBootstrapModule()); scanningWorkflow = injector.getInstance(DefaultScanningWorkflow.class); ScanResults scanResults = scanningWorkflow.run(buildScanTarget()); assertThat(scanResults.getScanStatus()).isEqualTo(ScanStatus.FAILED); assertThat(scanResults.getStatusMessage()) .contains("Plugin execution error on '/fake/PORT_SCAN/FailedPortScanner/v0.1'"); assertThat(scanResults.getScanFindingsList()).isEmpty(); } @Test public void run_whenServiceFingerprinterFailed_reusesNetworkServicesFromPortScan() throws ExecutionException, InterruptedException { Injector injector = Guice.createInjector( new HttpClientModule.Builder().build(), new PayloadGeneratorModule(new SecureRandom()), new FakeUtcClockModule(), new FakePluginExecutionModule(), new FakePortScannerBootstrapModule(), new FailedServiceFingerprinterBootstrapModule(), new FakeVulnDetectorBootstrapModule(), new FakeVulnDetectorBootstrapModule2()); scanningWorkflow = injector.getInstance(DefaultScanningWorkflow.class); ScanResults scanResults = scanningWorkflow.run(buildScanTarget()); assertThat(scanResults.getScanStatus()).isEqualTo(ScanStatus.SUCCEEDED); assertThat(scanResults.getTargetAlive()).isTrue(); assertThat(scanResults.getReconnaissanceReport().getNetworkServicesList()) .containsExactly( FakePortScanner.getFakeNetworkService(buildScanTarget().getNetworkEndpoint())); } @Test public void run_whenNoPortOrVulnReported_returnsHostDown() throws ExecutionException, InterruptedException { Injector injector = Guice.createInjector( new HttpClientModule.Builder().build(), new PayloadGeneratorModule(new SecureRandom()), new FakeUtcClockModule(), new FakePluginExecutionModule(), new FakePortScannerBootstrapModuleEmpty(), new FakeVulnDetectorBootstrapModuleEmpty()); scanningWorkflow = injector.getInstance(DefaultScanningWorkflow.class); ScanResults scanResults = scanningWorkflow.run(buildScanTarget()); assertThat(scanResults.getScanStatus()).isEqualTo(ScanStatus.SUCCEEDED); assertThat(scanResults.getReconnaissanceReport().getNetworkServicesList()).isEmpty(); assertThat(scanResults.getScanFindingsList()).isEmpty(); assertThat(scanResults.getTargetAlive()).isFalse(); } @Test public void run_whenServiceFingerprinterSucceeded_fillsReconnaissanceReportWithFingerprintResult() throws ExecutionException, InterruptedException { Injector injector = Guice.createInjector( new HttpClientModule.Builder().build(), new PayloadGeneratorModule(new SecureRandom()), new FakeUtcClockModule(), new FakePluginExecutionModule(), new FakePortScannerBootstrapModule(), new FakeServiceFingerprinterBootstrapModule(), new FakeVulnDetectorBootstrapModule(), new FakeVulnDetectorBootstrapModule2()); scanningWorkflow = injector.getInstance(DefaultScanningWorkflow.class); ScanResults scanResults = scanningWorkflow.run(buildScanTarget()); assertThat(scanResults.getScanStatus()).isEqualTo(ScanStatus.SUCCEEDED); assertThat(scanResults.getTargetAlive()).isTrue(); assertThat(scanResults.getReconnaissanceReport().getNetworkServicesList()) .containsExactly( FakeServiceFingerprinter.addWebServiceContext( FakePortScanner.getFakeNetworkService(buildScanTarget().getNetworkEndpoint()))); } @Test public void run_whenSomeVulnDetectorFailed_returnsPartiallySucceededScanResult() throws ExecutionException, InterruptedException { Injector injector = Guice.createInjector( new HttpClientModule.Builder().build(), new PayloadGeneratorModule(new SecureRandom()), new FakeUtcClockModule(), new FakePluginExecutionModule(), new FakePortScannerBootstrapModule(), new FakeServiceFingerprinterBootstrapModule(), new FakeVulnDetectorBootstrapModule(), new FakeRemoteVulnDetectorBootstrapModule(), new FailedVulnDetectorBootstrapModule(), new FailedRemoteVulnDetectorBootstrapModule()); scanningWorkflow = injector.getInstance(DefaultScanningWorkflow.class); ScanResults scanResults = scanningWorkflow.run(buildScanTarget()); assertThat(scanResults.getScanStatus()).isEqualTo(ScanStatus.PARTIALLY_SUCCEEDED); assertThat(scanResults.getStatusMessage()) .contains( "Failed plugins:\n" + "/fake/VULN_DETECTION/FailedVulnDetector/v0.1\n" + "/fake/REMOTE_VULN_DETECTION/FailedRemoteVulnDetector/v0.1"); assertThat(scanResults.getScanFindingsList()).hasSize(2); } @Test public void run_whenAllVulnDetectorFailed_returnsFailedScanResult() throws ExecutionException, InterruptedException { Injector injector = Guice.createInjector( new HttpClientModule.Builder().build(), new PayloadGeneratorModule(new SecureRandom()), new FakeUtcClockModule(), new FakePluginExecutionModule(), new FakePortScannerBootstrapModule(), new FakeServiceFingerprinterBootstrapModule(), new FailedVulnDetectorBootstrapModule(), new FailedRemoteVulnDetectorBootstrapModule()); scanningWorkflow = injector.getInstance(DefaultScanningWorkflow.class); ScanResults scanResults = scanningWorkflow.run(buildScanTarget()); assertThat(scanResults.getScanStatus()).isEqualTo(ScanStatus.FAILED); assertThat(scanResults.getStatusMessage()).contains("All VulnDetectors failed"); assertThat(scanResults.getScanFindingsList()).isEmpty(); } @Test public void run_whenNullScanTarget_throwsNullPointerException() { Injector injector = Guice.createInjector( new HttpClientModule.Builder().build(), new PayloadGeneratorModule(new SecureRandom()), new FakeUtcClockModule(), new FakePluginExecutionModule(), new FakeServiceFingerprinterBootstrapModule(), new FakeVulnDetectorBootstrapModule()); scanningWorkflow = injector.getInstance(DefaultScanningWorkflow.class); assertThrows(NullPointerException.class, () -> scanningWorkflow.run(null)); } private static ScanTarget buildScanTarget() { return ScanTarget.newBuilder().setNetworkEndpoint(forIp("1.2.3.4")).build(); } private static ScanTarget buildUriScanTarget() { return ScanTarget.newBuilder() .setNetworkService(buildUriNetworkService("https://localhost/function1")) .build(); } } ================================================ FILE: workflow/src/test/java/com/google/tsunami/workflow/ExecutionTracerTest.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.tsunami.workflow; import static com.google.common.truth.Truth.assertThat; import static com.google.tsunami.common.data.NetworkEndpointUtils.forIpAndPort; import static org.junit.Assert.assertThrows; import com.google.common.base.Stopwatch; import com.google.common.collect.ImmutableList; import com.google.common.testing.FakeTicker; import com.google.inject.Guice; import com.google.tsunami.common.net.http.HttpClientModule; import com.google.tsunami.plugin.PluginManager; import com.google.tsunami.plugin.PluginManager.PluginMatchingResult; import com.google.tsunami.plugin.PortScanner; import com.google.tsunami.plugin.VulnDetector; import com.google.tsunami.plugin.payload.PayloadGeneratorModule; import com.google.tsunami.plugin.testing.FakePortScannerBootstrapModule; import com.google.tsunami.plugin.testing.FakeServiceFingerprinterBootstrapModule; import com.google.tsunami.plugin.testing.FakeVulnDetectorBootstrapModule; import com.google.tsunami.plugin.testing.FakeVulnDetectorBootstrapModule2; import com.google.tsunami.proto.NetworkService; import com.google.tsunami.proto.ReconnaissanceReport; import com.google.tsunami.proto.ScanFinding; import com.google.tsunami.proto.ScanResults; import com.google.tsunami.proto.TransportProtocol; import java.security.SecureRandom; import java.time.Duration; import javax.inject.Inject; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** Tests for {@link ExecutionTracer}. */ @RunWith(JUnit4.class) public final class ExecutionTracerTest { private static final Duration TICK_DURATION = Duration.ofSeconds(1); private final FakeTicker ticker = new FakeTicker().setAutoIncrementStep(TICK_DURATION); private final Stopwatch portScanningTimer = Stopwatch.createUnstarted(ticker); private final Stopwatch serviceFingerprintingTimer = Stopwatch.createUnstarted(ticker); private final Stopwatch vulnerabilityDetectingTimer = Stopwatch.createUnstarted(ticker); @Inject PluginManager pluginManager; @Before public void setUp() { Guice.createInjector( new HttpClientModule.Builder().build(), new PayloadGeneratorModule(new SecureRandom()), new FakePortScannerBootstrapModule(), new FakePortScannerBootstrapModule(), new FakeServiceFingerprinterBootstrapModule(), new FakeVulnDetectorBootstrapModule(), new FakeVulnDetectorBootstrapModule2()) .injectMembers(this); } @Test public void startWorkflow_always_createExecutionTracerAtStartStage() { assertThat(ExecutionTracer.startWorkflow().getCurrentExecutionStage()) .isEqualTo(ExecutionStage.START); } @Test public void startPortScanning_always_setsExecutionTracerToCorrectState() { ExecutionTracer executionTracer = new ExecutionTracer( portScanningTimer, serviceFingerprintingTimer, vulnerabilityDetectingTimer); ImmutableList> installedPortScanners = pluginManager.getPortScanners(); executionTracer.startPortScanning(installedPortScanners); assertThat(portScanningTimer.isRunning()).isTrue(); assertThat(serviceFingerprintingTimer.isRunning()).isFalse(); assertThat(vulnerabilityDetectingTimer.isRunning()).isFalse(); assertThat(executionTracer.getCurrentExecutionStage()).isEqualTo(ExecutionStage.PORT_SCANNING); assertThat(executionTracer.getSelectedPortScanners()) .containsExactlyElementsIn(installedPortScanners); assertThat(executionTracer.getPortScanningStageRuntime()).isEqualTo(TICK_DURATION); } @Test public void startPortScanning_whenExecutionStageIsNotStart_throwsException() { ExecutionTracer executionTracer = new ExecutionTracer( portScanningTimer, serviceFingerprintingTimer, vulnerabilityDetectingTimer); ImmutableList> installedPortScanners = pluginManager.getPortScanners(); executionTracer.startPortScanning(installedPortScanners); assertThrows( IllegalStateException.class, () -> executionTracer.startPortScanning(installedPortScanners)); } @Test public void startServiceFingerprinting_always_setsExecutionTracerToCorrectState() { ExecutionTracer executionTracer = new ExecutionTracer( portScanningTimer, serviceFingerprintingTimer, vulnerabilityDetectingTimer); executionTracer.startPortScanning(pluginManager.getPortScanners()); // TODO(b/145315535): fill service fingerprinter data when plugin manager has the interface. executionTracer.startServiceFingerprinting(ImmutableList.of()); assertThat(portScanningTimer.isRunning()).isFalse(); assertThat(serviceFingerprintingTimer.isRunning()).isTrue(); assertThat(vulnerabilityDetectingTimer.isRunning()).isFalse(); assertThat(executionTracer.getCurrentExecutionStage()) .isEqualTo(ExecutionStage.SERVICE_FINGERPRINTING); assertThat(executionTracer.getSelectedServiceFingerprinters()).isEmpty(); assertThat(executionTracer.getServiceFingerprintingStageRuntime()).isEqualTo(TICK_DURATION); } @Test public void startServiceFingerprinting_whenStageNotPortScanning_throwsException() { ExecutionTracer executionTracer = new ExecutionTracer( portScanningTimer, serviceFingerprintingTimer, vulnerabilityDetectingTimer); assertThrows( IllegalStateException.class, () -> executionTracer.startServiceFingerprinting(ImmutableList.of())); } @Test public void startServiceFingerprinting_whenPortScanningTimerNotRunning_throwsException() { ExecutionTracer executionTracer = new ExecutionTracer( portScanningTimer, serviceFingerprintingTimer, vulnerabilityDetectingTimer); executionTracer.startPortScanning(pluginManager.getPortScanners()); portScanningTimer.stop(); assertThrows( IllegalStateException.class, () -> executionTracer.startServiceFingerprinting(ImmutableList.of())); } @Test public void startVulnerabilityDetecting_always_setsExecutionTracerToCorrectState() { ExecutionTracer executionTracer = new ExecutionTracer( portScanningTimer, serviceFingerprintingTimer, vulnerabilityDetectingTimer); executionTracer.startPortScanning(pluginManager.getPortScanners()); executionTracer.startServiceFingerprinting(ImmutableList.of()); ImmutableList> installedVulnDetectors = pluginManager.getVulnDetectors(ReconnaissanceReport.getDefaultInstance()); executionTracer.startVulnerabilityDetecting(installedVulnDetectors); assertThat(portScanningTimer.isRunning()).isFalse(); assertThat(serviceFingerprintingTimer.isRunning()).isFalse(); assertThat(vulnerabilityDetectingTimer.isRunning()).isTrue(); assertThat(executionTracer.getCurrentExecutionStage()) .isEqualTo(ExecutionStage.VULNERABILITY_DETECTING); assertThat(executionTracer.getSelectedVulnDetectors()) .containsExactlyElementsIn(installedVulnDetectors); assertThat(executionTracer.getVulnerabilityDetectingStageRuntime()).isEqualTo(TICK_DURATION); } @Test public void startVulnerabilityDetecting_whenStageNotFingerprinting_throwsException() { ExecutionTracer executionTracer = new ExecutionTracer( portScanningTimer, serviceFingerprintingTimer, vulnerabilityDetectingTimer); assertThrows( IllegalStateException.class, () -> executionTracer.startVulnerabilityDetecting( pluginManager.getVulnDetectors(ReconnaissanceReport.getDefaultInstance()))); } @Test public void startVulnerabilityDetecting_whenFingerprintingTimerNotRunning_throwsException() { ExecutionTracer executionTracer = new ExecutionTracer( portScanningTimer, serviceFingerprintingTimer, vulnerabilityDetectingTimer); executionTracer.startPortScanning(pluginManager.getPortScanners()); executionTracer.startServiceFingerprinting(ImmutableList.of()); serviceFingerprintingTimer.stop(); assertThrows( IllegalStateException.class, () -> executionTracer.startVulnerabilityDetecting( pluginManager.getVulnDetectors(ReconnaissanceReport.getDefaultInstance()))); } @Test public void setDone_always_setsExecutionTracerToCorrectState() { ExecutionTracer executionTracer = new ExecutionTracer( portScanningTimer, serviceFingerprintingTimer, vulnerabilityDetectingTimer); executionTracer.startPortScanning(pluginManager.getPortScanners()); executionTracer.startServiceFingerprinting(ImmutableList.of()); executionTracer.startVulnerabilityDetecting( pluginManager.getVulnDetectors(ReconnaissanceReport.getDefaultInstance())); executionTracer.setDone(); assertThat(portScanningTimer.isRunning()).isFalse(); assertThat(serviceFingerprintingTimer.isRunning()).isFalse(); assertThat(vulnerabilityDetectingTimer.isRunning()).isFalse(); assertThat(executionTracer.isDone()).isTrue(); } @Test public void setDone_whenStageNotVulnerabilityDetecting_throwsException() { ExecutionTracer executionTracer = new ExecutionTracer( portScanningTimer, serviceFingerprintingTimer, vulnerabilityDetectingTimer); assertThrows(IllegalStateException.class, executionTracer::setDone); } @Test public void setDone_whenVulnerabilityDetectingTimerNotRunning_throwsException() { ExecutionTracer executionTracer = new ExecutionTracer( portScanningTimer, serviceFingerprintingTimer, vulnerabilityDetectingTimer); executionTracer.startPortScanning(pluginManager.getPortScanners()); executionTracer.startServiceFingerprinting(ImmutableList.of()); executionTracer.startVulnerabilityDetecting( pluginManager.getVulnDetectors(ReconnaissanceReport.getDefaultInstance())); vulnerabilityDetectingTimer.stop(); assertThrows(IllegalStateException.class, executionTracer::setDone); } @Test public void forceDone_always_stopsAllTimerAndSetDoneStatus() { ExecutionTracer executionTracer = new ExecutionTracer( portScanningTimer, serviceFingerprintingTimer, vulnerabilityDetectingTimer); executionTracer.startPortScanning(pluginManager.getPortScanners()); executionTracer.forceDone(); assertThat(portScanningTimer.isRunning()).isFalse(); assertThat(serviceFingerprintingTimer.isRunning()).isFalse(); assertThat(vulnerabilityDetectingTimer.isRunning()).isFalse(); assertThat(executionTracer.isDone()).isTrue(); } @Test public void buildLoggableExecutionTrace_always_generatesExpectedMessage() { ReconnaissanceReport reconnaissanceReport = ReconnaissanceReport.newBuilder() .addNetworkServices( NetworkService.newBuilder() .setNetworkEndpoint(forIpAndPort("1.1.1.1", 80)) .setTransportProtocol(TransportProtocol.TCP) .setServiceName("http") .build()) .addNetworkServices( NetworkService.newBuilder() .setNetworkEndpoint(forIpAndPort("1.1.1.1", 22)) .setTransportProtocol(TransportProtocol.TCP) .setServiceName("ssh") .build()) .build(); ExecutionTracer executionTracer = new ExecutionTracer( portScanningTimer, serviceFingerprintingTimer, vulnerabilityDetectingTimer); executionTracer.startPortScanning(pluginManager.getPortScanners()); executionTracer.startServiceFingerprinting(ImmutableList.of()); executionTracer.startVulnerabilityDetecting( pluginManager.getVulnDetectors(reconnaissanceReport)); executionTracer.setDone(); String message = executionTracer.buildLoggableExecutionTrace( ScanResults.newBuilder().addScanFindings(ScanFinding.getDefaultInstance()).build()); assertThat(message) .isEqualTo( "Tsunami scanning workflow traces:\n" + " Port scanning phase (1.000 s) with 1 plugin(s):\n" + " /fake/PORT_SCAN/FakePortScanner/v0.1\n" + " Service fingerprinting phase (1.000 s) with 0 plugin(s):\n" + " \n" + " Vuln detection phase (1.000 s) with 2 plugin(s):\n" + " /fake/VULN_DETECTION/FakeVulnDetector/v0.1 was selected for the following" + " services: http (TCP, port 80), ssh (TCP, port 22)\n" + " /fake/VULN_DETECTION/FakeVulnDetector2/v0.1 was selected for the following" + " services: http (TCP, port 80), ssh (TCP, port 22)\n" + " # of detected vulnerability: 1."); } }