diff --git a/.gitignore b/.gitignore index 1707f86..6874ddd 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,6 @@ Cargo.lock *.pdb /.idea + +report.* +baseline.json diff --git a/Cargo.toml b/Cargo.toml index 020690b..ed6c2d0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,25 @@ [package] -name = "scale-testing" +name = "loadtest" version = "0.1.0" edition = "2021" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[workspace] -[dependencies] \ No newline at end of file +[dependencies] +anyhow = "1" +chrono = "0.4" +clap = { version = "4", features = ["derive", "env"] } +goose = "=0.17.3-dev" +goose-eggs = "0.5.3-dev" +humantime = "2" +log = "0.4" +openid = "0.14" +reqwest = "0.12" +tokio = { version = "1.38.0", features = ["sync"] } + +[patch.crates-io] +#goose = { path = "../../goose" } +#goose-eggs = { path = "../../goose-eggs" } + +goose = { git = "https://github.com/ctron/goose", branch = "feature/baseline_1" } +goose-eggs = { git = "https://github.com/ctron/goose-eggs", branch = "feature/uptick_deps_1" } diff --git a/README.md b/README.md index 4216e97..f1d8cca 100644 --- a/README.md +++ b/README.md @@ -1,88 +1,38 @@ -# scale-testing +# trustify loadtest -[![ci](https://github.com/trustification/scale-testing/actions/workflows/ci.yaml/badge.svg)](https://github.com/trustification/scale-testing/actions/workflows/ci.yaml) +A set of simple [goose](https://book.goose.rs/) load tests against the web and rest endpoints. -Utility for testing trustification at scale. +## quickstart -This tool is to help replicating existing SBOMs (SPDX or CycloneDX) file in order to augment an existing data set by multiplying the number SBOMs files. +1. Ensure trustify is running. -For instance let's say we have a total of 1000 SBMS (500 SPDX and 500) and we'd like to obtain a total of 10K SBOMs files for our scale test, so we can run the tool using a replication size of 10. +2. Set environment variables for OIDC authentication: + ```bash + export ISSUER_URL = "http://localhost:8090/realms/trustify" + export CLIENT_ID = "testing-user" + export CLIENT_SECRET = "****************" + ``` -The tool replicates existing SBOMs, by copying each file content and change its file name and its key records. + To change wait times between http invokes set the following env vars: -## Usage -After installing trustification/scale-testing repo, + ```bash + export WAIT_TIME_FROM = 1 + export WAIT_TIME_TO = 2 + ``` -We can run the tool, by providing the size of the replication, the source directory and the destination directory : + Alternately, for no wait times between http invokes set these env vars to 0. -We need to provide a source of SBOMs, here located in /SBOMs which contains SBOM files in json format. +3. To load trustify endpoints with 3 concurrent users. + ```bash + cargo run --release --bin loadtest -- --host http://localhost:8080 -u 3 + ``` -Also the target directory must not exists, this is to ensure we're not erasing an existing test set. + To stop load test hit [ctl-C], which should generate aggregate statistics. -```sh -$ rm -rf /data-set -$ cargo run -- 10 /SBOMs/ /data-set/ + To load trustify endpoints against 10 concurrent users, generating an html report. -The latter will replicate 10 times each SBOM file available in /SBOMs/. + ```bash + cargo run --release -- --host http://localhost:8080 --report-file=report.html --no-reset-metrics -u 10 + ``` -Each replicated SBOM file will be created under its corresponding batch directory under `/data-set`. - -> Example - -```sh -$ cargo run -- 2 ./SBOMs ./data-set - Compiling scale-testing v0.1.0 (/home/gildub/github.com/gildub/scale-testing) - Finished dev [unoptimized + debuginfo] target(s) in 0.24s - Running `target/debug/scale-testing 2 ./SBOMs ./data-set` -Replication multiplier 2 -Source directory ./SBOMs -Destination directory ./data-set -successfully wrote to metadata file -successfully wrote to metadata file -Amending version: "version": 1, -successfully wrote to ./data-set/batch1/A7ED160707AB4BC.replicate1.cdx.json -Amending version: "version": 1, -successfully wrote to ./data-set/batch2/A7ED160707AB4BC.replicate2.cdx.json -Amending name: "name": "quarkus-2.13", -Amending documentNameSpacekey: "documentNamespace": "https://access.redhat.com/security/data/sbom/beta/spdx/quarkus-2.13-1a6ac4c55918a44fb3bada1b7e7d12f887d67be4", -successfully wrote to ./data-set/batch1/quarkus-2.replicate1.13.json -Amending name: "name": "quarkus-2.13", -Amending documentNameSpacekey: "documentNamespace": "https://access.redhat.com/security/data/sbom/beta/spdx/quarkus-2.13-1a6ac4c55918a44fb3bada1b7e7d12f887d67be4", -successfully wrote to ./data-set/batch2/quarkus-2.replicate2.13.json -``` - -```sh -$ tree data-set/ -data-set/ -├── batch1 -│ ├── metadata -│ │ └── metadata.json -│ ├── A7ED160707AB4BC.replicate1.cdx.json -│ └── quarkus-2.replicate1.13.json -└── batch2 - ├── metadata - │ └── metadata.json - ├── A7ED160707AB4BC.replicate2.cdx.json - └── quarkus-2.replicate2.13.json - -5 directories, 6 files -``` - -## Using bombastic_walker - -### Prepare initial SBOMs data set - -Provide initial set of SBOMs files to be replicated, i.e `/SBOMs/`. -Use only SPDX files, for now, because the CycloneDX files tested were rejected with following error: `JSON: Unsupported CycloneDX version: 1.4` - -### Replicate SBOMs - -Run the replication tool to multiply your existing SBOM files set - -`cargo run -- 10 /SBOMs /data-set` - -### Use the replicated SBOMs - -The bombastic_walker could exploit each replicated SBOMs batch, for example in devmode : - -`RUST_LOG=info cargo run -p trust bombastic walker --sink http://localhost:8082 --source /data-set/batch1/ --devmode -3` +4. More goose run-time options [here](https://book.goose.rs/getting-started/runtime-options.html) diff --git a/loadtests/.gitignore b/loadtests/.gitignore deleted file mode 100644 index 662df16..0000000 --- a/loadtests/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -report.* -baseline.json diff --git a/loadtests/Cargo.toml b/loadtests/Cargo.toml deleted file mode 100644 index f609392..0000000 --- a/loadtests/Cargo.toml +++ /dev/null @@ -1,25 +0,0 @@ -[workspace] - -[package] -name = "loadtest" -version = "0.1.0" -edition = "2021" - -[dependencies] -anyhow = "1" -chrono = "0.4" -clap = { version = "4", features = ["derive", "env"] } -goose = "=0.17.3-dev" -goose-eggs = "0.5.3-dev" -humantime = "2" -log = "0.4" -openid = "0.14" -reqwest = "0.12" -tokio = { version = "1.38.0", features = ["sync"] } - -[patch.crates-io] -#goose = { path = "../../goose" } -#goose-eggs = { path = "../../goose-eggs" } - -goose = { git = "https://github.com/ctron/goose", branch = "feature/baseline_1" } -goose-eggs = { git = "https://github.com/ctron/goose-eggs", branch = "feature/uptick_deps_1" } diff --git a/loadtests/README.md b/loadtests/README.md deleted file mode 100644 index f1d8cca..0000000 --- a/loadtests/README.md +++ /dev/null @@ -1,38 +0,0 @@ -# trustify loadtest - -A set of simple [goose](https://book.goose.rs/) load tests against the web and rest endpoints. - -## quickstart - -1. Ensure trustify is running. - -2. Set environment variables for OIDC authentication: - ```bash - export ISSUER_URL = "http://localhost:8090/realms/trustify" - export CLIENT_ID = "testing-user" - export CLIENT_SECRET = "****************" - ``` - - To change wait times between http invokes set the following env vars: - - ```bash - export WAIT_TIME_FROM = 1 - export WAIT_TIME_TO = 2 - ``` - - Alternately, for no wait times between http invokes set these env vars to 0. - -3. To load trustify endpoints with 3 concurrent users. - ```bash - cargo run --release --bin loadtest -- --host http://localhost:8080 -u 3 - ``` - - To stop load test hit [ctl-C], which should generate aggregate statistics. - - To load trustify endpoints against 10 concurrent users, generating an html report. - - ```bash - cargo run --release -- --host http://localhost:8080 --report-file=report.html --no-reset-metrics -u 10 - ``` - -4. More goose run-time options [here](https://book.goose.rs/getting-started/runtime-options.html) diff --git a/loadtests/src/main.rs b/loadtests/src/main.rs deleted file mode 100644 index eb2ce82..0000000 --- a/loadtests/src/main.rs +++ /dev/null @@ -1,135 +0,0 @@ -extern crate core; - -// The simplest loadtest example -mod graphql; -mod oidc; -mod restapi; -mod website; - -use crate::{ - graphql::graphql_query_advisory, - oidc::{OpenIdTokenProvider, OpenIdTokenProviderConfigArguments}, - restapi::{ - get_advisory, get_importer, get_oganizations, get_packages, get_products, get_sboms, - get_vulnerabilities, search_packages, - }, - website::{ - website_advisories, website_importers, website_index, website_openapi, website_packages, - website_sboms, - }, -}; -use goose::prelude::*; -use std::{str::FromStr, time::Duration}; - -#[tokio::main] -async fn main() -> Result<(), GooseError> { - let wait_time_from: u64 = std::env::var("WAIT_TIME_FROM") - .map(|s| s.parse().unwrap_or(5)) - .unwrap_or(5); - let wait_time_to: u64 = std::env::var("WAIT_TIME_TO") - .map(|s| s.parse().unwrap_or(15)) - .unwrap_or(15); - - GooseAttack::initialize()? - .register_scenario( - scenario!("WebsiteUser") - // .set_weight(1)? - .register_transaction(transaction!(setup_custom_client).set_name("logon")) - // After each transactions runs, sleep randomly from 5 to 15 seconds. - .set_wait_time( - Duration::from_secs(wait_time_from), - Duration::from_secs(wait_time_to), - )? - .register_transaction(transaction!(website_index).set_name("/index")) - .register_transaction(transaction!(website_openapi).set_name("/openapi")) - .register_transaction(transaction!(website_sboms).set_name("/sboms")) - .register_transaction(transaction!(website_packages).set_name("/packages")) - .register_transaction(transaction!(website_advisories).set_name("/advisories")) - .register_transaction(transaction!(website_importers).set_name("/importers")), - ) - .register_scenario( - scenario!("RestAPIUser") - // .set_weight(1)? - .register_transaction(transaction!(setup_custom_client).set_name("logon")) - // After each transactions runs, sleep randomly from 5 to 15 seconds. - .set_wait_time( - Duration::from_secs(wait_time_from), - Duration::from_secs(wait_time_to), - )? - .register_transaction( - transaction!(get_oganizations).set_name("/api/v1/organization"), - ) - .register_transaction(transaction!(get_advisory).set_name("/api/v1/advisory")) - .register_transaction( - transaction!(get_vulnerabilities).set_name("/api/v1/vulnerability"), - ) - .register_transaction(transaction!(get_importer).set_name("/api/v1/importer")) - .register_transaction(transaction!(get_packages).set_name("/api/v1/purl")) - .register_transaction(transaction!(search_packages).set_name("/api/v1/purl?q=curl")) - .register_transaction(transaction!(get_products).set_name("/api/v1/product")) - .register_transaction(transaction!(get_sboms).set_name("/api/v1/sbom")), - ) - .register_scenario( - scenario!("GraphQLUser") - // .set_weight(1)? - .register_transaction(transaction!(setup_custom_client).set_name("logon")) - // After each transactions runs, sleep randomly from 5 to 15 seconds. - .set_wait_time( - Duration::from_secs(wait_time_from), - Duration::from_secs(wait_time_to), - )? - .register_transaction( - transaction!(graphql_query_advisory).set_name("query advisory with /graphql"), - ), - ) - .execute() - .await?; - - Ok(()) -} - -async fn setup_custom_client(user: &mut GooseUser) -> TransactionResult { - use reqwest::{header, Client}; - - log::info!("Creating a new custom client"); - - let issuer_url = std::env::var("ISSUER_URL").unwrap(); - let client_id = std::env::var("CLIENT_ID").unwrap(); - let client_secret = std::env::var("CLIENT_SECRET").unwrap(); - let refresh_before = std::env::var("OIDC_REFRESH_BEFORE").unwrap_or_else(|_| "30s".to_string()); - let refresh_before = - humantime::Duration::from_str(&refresh_before).expect("OIDC_REFRESH_BEFORE must parse"); - - let provider = OpenIdTokenProvider::with_config(OpenIdTokenProviderConfigArguments { - client_id, - client_secret, - issuer_url, - refresh_before, - tls_insecure: false, - }) - .await - .expect("discover OIDC client"); - - let auth_token: String = provider - .provide_token() - .await - .expect("get OIDC token") - .access_token; - - let mut headers = header::HeaderMap::new(); - headers.insert( - "Authorization", - header::HeaderValue::from_str(&format!("Bearer {auth_token}")).unwrap(), - ); - - // Build a custom client. - let builder = Client::builder() - .default_headers(headers) - .user_agent("loadtest-ua") - .timeout(Duration::from_secs(30)); - - // Assign the custom client to this GooseUser. - user.set_client_builder(builder).await?; - - Ok(()) -} diff --git a/replicator/.clippy.toml b/replicator/.clippy.toml new file mode 100644 index 0000000..0358cdb --- /dev/null +++ b/replicator/.clippy.toml @@ -0,0 +1,2 @@ +allow-unwrap-in-tests = true +allow-expect-in-tests = true diff --git a/replicator/.gitignore b/replicator/.gitignore new file mode 100644 index 0000000..6874ddd --- /dev/null +++ b/replicator/.gitignore @@ -0,0 +1,19 @@ +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb + +/.idea + +report.* +baseline.json diff --git a/replicator/Cargo.toml b/replicator/Cargo.toml new file mode 100644 index 0000000..c95e039 --- /dev/null +++ b/replicator/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "scale-testing" +version = "0.1.0" +edition = "2021" + +[workspace] diff --git a/replicator/LICENSE b/replicator/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/replicator/LICENSE @@ -0,0 +1,201 @@ + 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. diff --git a/replicator/README.md b/replicator/README.md new file mode 100644 index 0000000..4216e97 --- /dev/null +++ b/replicator/README.md @@ -0,0 +1,88 @@ +# scale-testing + +[![ci](https://github.com/trustification/scale-testing/actions/workflows/ci.yaml/badge.svg)](https://github.com/trustification/scale-testing/actions/workflows/ci.yaml) + +Utility for testing trustification at scale. + +This tool is to help replicating existing SBOMs (SPDX or CycloneDX) file in order to augment an existing data set by multiplying the number SBOMs files. + +For instance let's say we have a total of 1000 SBMS (500 SPDX and 500) and we'd like to obtain a total of 10K SBOMs files for our scale test, so we can run the tool using a replication size of 10. + +The tool replicates existing SBOMs, by copying each file content and change its file name and its key records. + +## Usage +After installing trustification/scale-testing repo, + +We can run the tool, by providing the size of the replication, the source directory and the destination directory : + +We need to provide a source of SBOMs, here located in /SBOMs which contains SBOM files in json format. + +Also the target directory must not exists, this is to ensure we're not erasing an existing test set. + +```sh +$ rm -rf /data-set +$ cargo run -- 10 /SBOMs/ /data-set/ + +The latter will replicate 10 times each SBOM file available in /SBOMs/. + +Each replicated SBOM file will be created under its corresponding batch directory under `/data-set`. + +> Example + +```sh +$ cargo run -- 2 ./SBOMs ./data-set + Compiling scale-testing v0.1.0 (/home/gildub/github.com/gildub/scale-testing) + Finished dev [unoptimized + debuginfo] target(s) in 0.24s + Running `target/debug/scale-testing 2 ./SBOMs ./data-set` +Replication multiplier 2 +Source directory ./SBOMs +Destination directory ./data-set +successfully wrote to metadata file +successfully wrote to metadata file +Amending version: "version": 1, +successfully wrote to ./data-set/batch1/A7ED160707AB4BC.replicate1.cdx.json +Amending version: "version": 1, +successfully wrote to ./data-set/batch2/A7ED160707AB4BC.replicate2.cdx.json +Amending name: "name": "quarkus-2.13", +Amending documentNameSpacekey: "documentNamespace": "https://access.redhat.com/security/data/sbom/beta/spdx/quarkus-2.13-1a6ac4c55918a44fb3bada1b7e7d12f887d67be4", +successfully wrote to ./data-set/batch1/quarkus-2.replicate1.13.json +Amending name: "name": "quarkus-2.13", +Amending documentNameSpacekey: "documentNamespace": "https://access.redhat.com/security/data/sbom/beta/spdx/quarkus-2.13-1a6ac4c55918a44fb3bada1b7e7d12f887d67be4", +successfully wrote to ./data-set/batch2/quarkus-2.replicate2.13.json +``` + +```sh +$ tree data-set/ +data-set/ +├── batch1 +│ ├── metadata +│ │ └── metadata.json +│ ├── A7ED160707AB4BC.replicate1.cdx.json +│ └── quarkus-2.replicate1.13.json +└── batch2 + ├── metadata + │ └── metadata.json + ├── A7ED160707AB4BC.replicate2.cdx.json + └── quarkus-2.replicate2.13.json + +5 directories, 6 files +``` + +## Using bombastic_walker + +### Prepare initial SBOMs data set + +Provide initial set of SBOMs files to be replicated, i.e `/SBOMs/`. +Use only SPDX files, for now, because the CycloneDX files tested were rejected with following error: `JSON: Unsupported CycloneDX version: 1.4` + +### Replicate SBOMs + +Run the replication tool to multiply your existing SBOM files set + +`cargo run -- 10 /SBOMs /data-set` + +### Use the replicated SBOMs + +The bombastic_walker could exploit each replicated SBOMs batch, for example in devmode : + +`RUST_LOG=info cargo run -p trust bombastic walker --sink http://localhost:8082 --source /data-set/batch1/ --devmode -3` diff --git a/replicator/rust-toolchain.toml b/replicator/rust-toolchain.toml new file mode 100644 index 0000000..e8d3cdd --- /dev/null +++ b/replicator/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "1.80.1" +components = ["rustfmt", "clippy"] diff --git a/src/config.rs b/replicator/src/config.rs similarity index 100% rename from src/config.rs rename to replicator/src/config.rs diff --git a/replicator/src/main.rs b/replicator/src/main.rs new file mode 100644 index 0000000..54f3b76 --- /dev/null +++ b/replicator/src/main.rs @@ -0,0 +1,25 @@ +use std::env; +use std::process; +use std::process::ExitCode; + +mod config; +mod replicator; + +fn main() -> ExitCode { + let args: Vec = env::args().collect(); + + let config = config::Config::build(&args).unwrap_or_else(|err| { + println!("Problem parsing arguments: {err}"); + process::exit(1); + }); + + config.validate(); + + let replication = replicator::Replication::new(config.clone()); + replication.run().unwrap_or_else(|err| { + println!("Application error: {err}"); + process::exit(1) + }); + + 0.into() +} diff --git a/src/replicator.rs b/replicator/src/replicator.rs similarity index 100% rename from src/replicator.rs rename to replicator/src/replicator.rs diff --git a/rust-toolchain.toml b/rust-toolchain.toml index d322aab..e8d3cdd 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] -channel = "1.78.0" -components = [ "rustfmt", "clippy" ] +channel = "1.80.1" +components = ["rustfmt", "clippy"] diff --git a/loadtests/src/graphql.rs b/src/graphql.rs similarity index 100% rename from loadtests/src/graphql.rs rename to src/graphql.rs diff --git a/src/main.rs b/src/main.rs index 54f3b76..4de6c5a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,25 +1,162 @@ -use std::env; -use std::process; -use std::process::ExitCode; +extern crate core; -mod config; -mod replicator; +// The simplest loadtest example +mod graphql; +mod oidc; +mod restapi; +mod website; -fn main() -> ExitCode { - let args: Vec = env::args().collect(); +use crate::{ + graphql::graphql_query_advisory, + oidc::{OpenIdTokenProvider, OpenIdTokenProviderConfigArguments}, + restapi::{ + get_advisory, get_importer, get_oganizations, get_packages, get_products, get_sboms, + get_vulnerabilities, search_packages, + }, + website::{ + website_advisories, website_importers, website_index, website_openapi, website_packages, + website_sboms, + }, +}; +use anyhow::Context; +use goose::prelude::*; +use std::{str::FromStr, sync::Arc, time::Duration}; - let config = config::Config::build(&args).unwrap_or_else(|err| { - println!("Problem parsing arguments: {err}"); - process::exit(1); - }); +#[tokio::main] +async fn main() -> Result<(), anyhow::Error> { + let wait_time_from: u64 = std::env::var("WAIT_TIME_FROM") + .map(|s| s.parse().unwrap_or(5)) + .unwrap_or(5); + let wait_time_to: u64 = std::env::var("WAIT_TIME_TO") + .map(|s| s.parse().unwrap_or(15)) + .unwrap_or(15); - config.validate(); + let provider = create_oidc_provider().await?; + let custom_client = Transaction::new(Arc::new(move |user| { + let provider = provider.clone(); + Box::pin(async move { setup_custom_client(&provider, user).await }) + })); - let replication = replicator::Replication::new(config.clone()); - replication.run().unwrap_or_else(|err| { - println!("Application error: {err}"); - process::exit(1) - }); + GooseAttack::initialize()? + .register_scenario( + scenario!("WebsiteUser") + // .set_weight(1)? + .register_transaction(custom_client.clone().set_name("logon")) + // After each transactions runs, sleep randomly from 5 to 15 seconds. + .set_wait_time( + Duration::from_secs(wait_time_from), + Duration::from_secs(wait_time_to), + )? + .register_transaction(transaction!(website_index).set_name("/index")) + .register_transaction(transaction!(website_openapi).set_name("/openapi")) + .register_transaction(transaction!(website_sboms).set_name("/sboms")) + .register_transaction(transaction!(website_packages).set_name("/packages")) + .register_transaction(transaction!(website_advisories).set_name("/advisories")) + .register_transaction(transaction!(website_importers).set_name("/importers")), + ) + .register_scenario( + scenario!("RestAPIUser") + // .set_weight(1)? + .register_transaction(custom_client.clone().set_name("logon")) + // After each transactions runs, sleep randomly from 5 to 15 seconds. + .set_wait_time( + Duration::from_secs(wait_time_from), + Duration::from_secs(wait_time_to), + )? + .register_transaction( + transaction!(get_oganizations).set_name("/api/v1/organization"), + ) + .register_transaction(transaction!(get_advisory).set_name("/api/v1/advisory")) + .register_transaction( + transaction!(get_vulnerabilities).set_name("/api/v1/vulnerability"), + ) + .register_transaction(transaction!(get_importer).set_name("/api/v1/importer")) + .register_transaction(transaction!(get_packages).set_name("/api/v1/purl")) + .register_transaction(transaction!(search_packages).set_name("/api/v1/purl?q=curl")) + .register_transaction(transaction!(get_products).set_name("/api/v1/product")) + .register_transaction(transaction!(get_sboms).set_name("/api/v1/sbom")), + ) + .register_scenario( + scenario!("GraphQLUser") + // .set_weight(1)? + .register_transaction(custom_client.set_name("logon")) + // After each transactions runs, sleep randomly from 5 to 15 seconds. + .set_wait_time( + Duration::from_secs(wait_time_from), + Duration::from_secs(wait_time_to), + )? + .register_transaction( + transaction!(graphql_query_advisory).set_name("query advisory with /graphql"), + ), + ) + .execute() + .await?; - 0.into() + Ok(()) +} + +async fn create_oidc_provider() -> anyhow::Result { + let issuer_url = std::env::var("ISSUER_URL").context("Missing env-var 'ISSUER_URL'")?; + let client_id = std::env::var("CLIENT_ID").context("Missing env-var 'CLIENT_ID'")?; + let client_secret = + std::env::var("CLIENT_SECRET").context("Missing env-var 'CLIENT_SECRET'")?; + let refresh_before = std::env::var("OIDC_REFRESH_BEFORE").unwrap_or_else(|_| "30s".to_string()); + let refresh_before = + humantime::Duration::from_str(&refresh_before).context("OIDC_REFRESH_BEFORE must parse")?; + + let provider = OpenIdTokenProvider::with_config(OpenIdTokenProviderConfigArguments { + client_id, + client_secret, + issuer_url, + refresh_before, + tls_insecure: false, + }) + .await + .context("discover OIDC client")?; + + Ok(provider) +} + +// required until https://github.com/tag1consulting/goose/pull/605 is merged +#[allow(clippy::expect_used)] +async fn setup_custom_client( + provider: &OpenIdTokenProvider, + user: &mut GooseUser, +) -> TransactionResult { + set_custom_client(provider, user) + .await + .expect("Failed to set up client"); + Ok(()) +} + +async fn set_custom_client( + provider: &OpenIdTokenProvider, + user: &mut GooseUser, +) -> anyhow::Result<()> { + use reqwest::{header, Client}; + + log::info!("Creating a new custom client"); + + let auth_token: String = provider + .provide_token() + .await + .context("get OIDC token")? + .access_token; + + let mut headers = header::HeaderMap::new(); + headers.insert( + "Authorization", + header::HeaderValue::from_str(&format!("Bearer {auth_token}"))?, + ); + + // Build a custom client. + let builder = Client::builder() + .default_headers(headers) + .user_agent("loadtest-ua") + .timeout(Duration::from_secs(30)); + + // Assign the custom client to this GooseUser. + user.set_client_builder(builder).await?; + + Ok(()) } diff --git a/loadtests/src/oidc.rs b/src/oidc.rs similarity index 100% rename from loadtests/src/oidc.rs rename to src/oidc.rs diff --git a/loadtests/src/restapi.rs b/src/restapi.rs similarity index 100% rename from loadtests/src/restapi.rs rename to src/restapi.rs diff --git a/loadtests/src/website.rs b/src/website.rs similarity index 100% rename from loadtests/src/website.rs rename to src/website.rs