├── .github ├── dependabot.yml └── workflows │ ├── releases.yml │ ├── rust.yml │ └── scorecard.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── SECURITY-INSIGHTS.yml ├── SECURITY.md ├── docs └── design │ └── project-state-store.md ├── rust-toolchain.toml ├── shell.nix ├── skootrs-bin ├── Cargo.toml ├── README.md └── src │ ├── helpers.rs │ └── main.rs ├── skootrs-lib ├── Cargo.toml ├── README.md ├── src │ ├── lib.rs │ └── service │ │ ├── ecosystem.rs │ │ ├── facet.rs │ │ ├── mod.rs │ │ ├── output.rs │ │ ├── project.rs │ │ ├── repo.rs │ │ └── source.rs └── templates │ ├── Dockerfile.goreleaser │ ├── LICENSE │ ├── README.md │ ├── SECURITY.prerelease.md │ ├── cifuzz.yml │ ├── codeql.yml │ ├── dependabot.yml │ ├── go.gitignore │ ├── go.releases.yml │ ├── goreleaser.yml │ ├── main.go.tmpl │ └── scorecard.yml ├── skootrs-model ├── Cargo.toml ├── README.md └── src │ ├── cd_events │ ├── mod.rs │ └── repo_created.rs │ ├── lib.rs │ ├── security_insights │ ├── insights10.rs │ └── mod.rs │ └── skootrs │ ├── facet.rs │ ├── label.rs │ └── mod.rs ├── skootrs-rest ├── Cargo.toml ├── README.md └── src │ ├── lib.rs │ └── server │ ├── mod.rs │ ├── project.rs │ └── rest.rs └── skootrs-statestore ├── Cargo.toml ├── README.md └── src └── lib.rs /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "cargo" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" -------------------------------------------------------------------------------- /.github/workflows/releases.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2022 The GUAC Authors. 3 | # Copyright 2024 The Skootrs Authors. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | name: release 17 | 18 | on: 19 | workflow_dispatch: # testing only, trigger manually to test it works 20 | push: 21 | branches: 22 | - main 23 | tags: 24 | - "v*" 25 | 26 | permissions: 27 | actions: read # for detecting the Github Actions environment. 28 | contents: read 29 | 30 | jobs: 31 | cargo: 32 | permissions: 33 | contents: write # To upload assets to release. 34 | packages: write # To publish container images to GHCR 35 | id-token: write # needed for signing the images with GitHub OIDC Token 36 | runs-on: ubuntu-latest 37 | outputs: 38 | hashes: ${{ steps.hash.outputs.hashes }} 39 | steps: 40 | - name: Checkout 41 | uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 42 | with: 43 | fetch-depth: 0 44 | # TODO: This is currently isn't used. Working on creating container images for Rust. 45 | - name: Login to GitHub Container Registry 46 | uses: docker/login-action@e92390c5fb421da1463c202d546fed0ec5c39f20 # v3.1.0 47 | with: 48 | registry: ghcr.io 49 | username: ${{ github.actor }} 50 | password: ${{ secrets.GITHUB_TOKEN }} 51 | # TODO: This currently isn't used. Working on creating signign container images for Rust. 52 | - name: Install cosign 53 | uses: sigstore/cosign-installer@59acb6260d9c0ba8f4a2f9d9b48431a222b68e20 # main 54 | - name: Setup Rust 55 | uses: dtolnay/rust-toolchain@nightly 56 | - name: Run Cargo Build for snapshot release 57 | if: ${{ !startsWith(github.ref, 'refs/tags/') }} 58 | id: run-cargo-snapshot 59 | run: cargo build --verbose && cargo test --verbose 60 | - name: Run Cargo Build for versioned Release 61 | if: startsWith(github.ref, 'refs/tags/') 62 | id: run-cargo-release 63 | run: cargo build --verbose --release && cargo test --verbose 64 | - name: Upload Release Asset 65 | env: 66 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 67 | if: startsWith(github.ref, 'refs/tags/') 68 | run: gh release upload ${{ github.ref_name }} target/release/skootrs 69 | # TODO: Don't make this hardcoded. 70 | - name: Generate hashes 71 | id: hash 72 | if: startsWith(github.ref, 'refs/tags/') 73 | run: | 74 | hashes=$(sha256sum target/release/skootrs | base64 -w0) 75 | echo "hashes=$hashes" >> $GITHUB_OUTPUT 76 | - name: Setup cargo release 77 | if: startsWith(github.ref, 'refs/tags/') 78 | run: cargo install cargo-release 79 | - name: Public to crates.io 80 | if: startsWith(github.ref, 'refs/tags/') 81 | env: 82 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 83 | run: | 84 | cargo release --execute --no-confirm 85 | sbom: 86 | permissions: 87 | contents: write 88 | runs-on: ubuntu-latest 89 | needs: [cargo] 90 | steps: 91 | - name: Checkout 92 | uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 93 | with: 94 | fetch-depth: 0 95 | - name: Setup cargo 96 | uses: actions-rs/toolchain@v1 97 | with: 98 | toolchain: nightly 99 | - name: Install cargo sbom 100 | run: cargo install cargo-sbom 101 | - name: Generate SBOM 102 | run: cargo sbom > skootrs.spdx.json 103 | - name: Upload SBOM 104 | if: startsWith(github.ref, 'refs/tags/') 105 | uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 106 | with: 107 | name: skootrs.spdx.json 108 | path: skootrs.spdx.json 109 | - name: Push SBOM to release 110 | env: 111 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 112 | if: startsWith(github.ref, 'refs/tags/') 113 | run: gh release upload ${{ github.ref_name }} skootrs.spdx.json 114 | provenance-bins: 115 | permissions: 116 | id-token: write 117 | actions: read 118 | contents: write 119 | packages: write 120 | name: generate provenance for binaries 121 | needs: [cargo] 122 | if: startsWith(github.ref, 'refs/tags/') 123 | uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.10.0 # must use semver here 124 | with: 125 | base64-subjects: "${{ needs.cargo.outputs.hashes }}" 126 | upload-assets: true -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: dtolnay/rust-toolchain@nightly 20 | - run: cargo build --verbose 21 | test: 22 | runs-on: ubuntu-latest 23 | needs: build 24 | steps: 25 | - uses: actions/checkout@v4 26 | - uses: dtolnay/rust-toolchain@nightly 27 | - run: cargo test --verbose 28 | sbom: 29 | runs-on: ubuntu-latest 30 | needs: build 31 | steps: 32 | - uses: actions/checkout@v4 33 | - uses: dtolnay/rust-toolchain@nightly 34 | - run: cargo install cargo-sbom 35 | - run: cargo sbom > skootrs.spdx.json -------------------------------------------------------------------------------- /.github/workflows/scorecard.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. They are provided 2 | # by a third-party and are governed by separate terms of service, privacy 3 | # policy, and support documentation. 4 | 5 | name: Scorecard supply-chain security 6 | on: 7 | # For Branch-Protection check. Only the default branch is supported. See 8 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection 9 | branch_protection_rule: 10 | # To guarantee Maintained check is occasionally updated. See 11 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained 12 | schedule: 13 | - cron: '17 18 * * 4' 14 | push: 15 | branches: [ "main" ] 16 | 17 | # Declare default permissions as read only. 18 | permissions: read-all 19 | 20 | jobs: 21 | analysis: 22 | name: Scorecard analysis 23 | runs-on: ubuntu-latest 24 | permissions: 25 | # Needed to upload the results to code-scanning dashboard. 26 | security-events: write 27 | # Needed to publish results and get a badge (see publish_results below). 28 | id-token: write 29 | # Uncomment the permissions below if installing in a private repository. 30 | # contents: read 31 | # actions: read 32 | 33 | steps: 34 | - name: "Checkout code" 35 | uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 36 | with: 37 | persist-credentials: false 38 | 39 | - name: "Run analysis" 40 | uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736 # v2.3.1 41 | with: 42 | results_file: results.sarif 43 | results_format: sarif 44 | # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: 45 | # - you want to enable the Branch-Protection check on a *public* repository, or 46 | # - you are installing Scorecard on a *private* repository 47 | # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat. 48 | # repo_token: $\{\{ secrets.SCORECARD_TOKEN \}\} 49 | 50 | # Public repositories: 51 | # - Publish results to OpenSSF REST API for easy access by consumers 52 | # - Allows the repository to include the Scorecard badge. 53 | # - See https://github.com/ossf/scorecard-action#publishing-results. 54 | # For private repositories: 55 | # - `publish_results` will always be set to `false`, regardless 56 | # of the value entered here. 57 | publish_results: true 58 | 59 | # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF 60 | # format to the repository Actions tab. 61 | - name: "Upload artifact" 62 | uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 63 | with: 64 | name: SARIF file 65 | path: results.sarif 66 | retention-days: 5 67 | 68 | # Upload the results to GitHub's code scanning dashboard. 69 | - name: "Upload to code-scanning" 70 | uses: github/codeql-action/upload-sarif@4355270be187e1b672a7a1c7c7bae5afdc1ab94a # v2.13.4 71 | with: 72 | sarif_file: results.sarif -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | .vscode 3 | .DS_Store 4 | state.db 5 | .envrc 6 | skootcache -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "skootrs-bin", 4 | "skootrs-lib", 5 | "skootrs-model", 6 | # "skootrs-rest", 7 | "skootrs-statestore" 8 | ] 9 | 10 | resolver = "2" 11 | 12 | [workspace.lints.rust] 13 | unsafe_code = "forbid" 14 | missing_docs = "warn" 15 | 16 | [workspace.lints.clippy] 17 | enum_glob_use = "deny" 18 | pedantic = "deny" 19 | nursery = "deny" 20 | unwrap_used = "deny" 21 | missing_errors_doc = "allow" 22 | module_name_repetitions = "allow" 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [2023] [Skootrs Authors] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Skootrs 2 | 3 | A CLI tool for creating secure-by-design/default source repos. 4 | 5 | **Note**: Skootrs is still pre-beta. The API will change often, and you should still audit what Skootrs is doing to ensure projects created by Skootrs are implementing all the security practices that it claims it does. 6 | 7 | - [Discord](https://discord.gg/ea74aBray2) 8 | 9 | ## Pre-reqs 10 | 11 | **Note**: These pre-reqs will change often as the tool develops and matures 12 | - Rust nightly >=1.77 - [Read more](https://www.rust-lang.org/tools/install) 13 | - GitHub token with the following permissions: `admin:org, admin:repo_hook, admin:ssh_signing_key, audit_log, delete_repo, repo, workflow, write:packages` in the `GITHUB_TOKEN` environment variable. 14 | 15 | ## Installing 16 | 17 | For releases you can download the `skootrs` binary from releases or run `cargo install skootrs-bin` 18 | 19 | For dev you can clone this repo and run `cargo install --path skootrs-bin` from the root of the repo. 20 | 21 | ## Running Skootrs 22 | 23 | ```shell 24 | $ cargo run 25 | Skootrs is a CLI tool for creating and managing secure-by-default projects. The commands are using noun-verb syntax. So the commands are structured like: `skootrs `. For example, `skootrs project create` 26 | 27 | Usage: skootrs 28 | 29 | Commands: 30 | project Project commands 31 | facet Facet commands 32 | output Output commands 33 | daemon Daemon commands 34 | help Print this message or the help of the given subcommand(s) 35 | 36 | Options: 37 | -h, --help Print help (see more with '--help') 38 | ``` 39 | 40 | Project: 41 | ```shell 42 | Usage: skootrs project 43 | 44 | Project commands 45 | 46 | Usage: skootrs project 47 | 48 | Commands: 49 | create Create a new project 50 | get Get the metadata for a particular project 51 | update Update a project 52 | archive Archive a project 53 | list List all the projects known to the local Skootrs 54 | help Print this message or the help of the given subcommand(s) 55 | ``` 56 | 57 | Facet: 58 | ```shell 59 | Facet commands 60 | 61 | Usage: skootrs facet 62 | 63 | Commands: 64 | get Get the data for a facet of a particular project 65 | list List all the facets that belong to a particular project 66 | help Print this message or the help of the given subcommand(s) 67 | ``` 68 | 69 | Output: 70 | ```shell 71 | Output commands 72 | 73 | Usage: skootrs output 74 | 75 | Commands: 76 | get Get the data for a release output of a particular project 77 | list List all the release outputs that belong to a particular project 78 | help Print this message or the help of the given subcommand(s) 79 | ``` 80 | 81 | Daemon: 82 | ```shell 83 | Daemon commands 84 | 85 | Usage: skootrs daemon 86 | 87 | Commands: 88 | start Start the REST server 89 | help Print this message or the help of the given subcommand(s) 90 | ``` 91 | 92 | To get pretty printing of the logs which are in [bunyan](https://github.com/trentm/node-bunyan) format I recommend piping the skootrs into the bunyan cli. I recommend using [bunyan-rs](https://github.com/LukeMathWalker/bunyan). For example: 93 | 94 | ```shell 95 | $ cargo run project create | bunyan ~/Projects/skootrs 96 | Finished dev [unoptimized + debuginfo] target(s) in 0.19s 97 | Running `target/debug/skootrs-bin create` 98 | > The name of the repository skoot-test-bunyan 99 | > The description of the repository asdf 100 | > Select an organization mlieberman85 101 | > Select a language Go 102 | [2024-02-08T05:34:11.249Z] INFO: skootrs/16973 on Michaels-MBP-2.localdomain: Github Repo Created: skoot-test-bunyan (file=skootrs-lib/src/service/repo.rs,line=81,target=skootrs_lib::service::repo) 103 | [2024-02-08T05:34:11.251Z] INFO: skootrs/16973 on Michaels-MBP-2.localdomain: {"context":{"id":"mlieberman85/skoot-test-bunyan","source":"skootrs.github.creator","timestamp":"2024-02-08T05:34:11.250869Z","type":"dev.cdevents.repository.created.0.1.1","version":"0.3.0"},"subject":{"content":{"name":"skoot-test-bunyan","owner":"mlieberman85","url":"https://github.com/mlieberman85/skoot-test-bunyan","viewUrl":"https://github.com/mlieberman85/skoot-test-bunyan"},"id":"mlieberman85/skoot-test-bunyan","source":"skootrs.github.creator","type":"repository"}} (file=skootrs-lib/src/service/repo.rs,line=106,target=skootrs_lib::service::repo) 104 | [2024-02-08T05:34:11.675Z] INFO: skootrs/16973 on Michaels-MBP-2.localdomain: Initialized go module for skoot-test-bunyan (file=skootrs-lib/src/service/ecosystem.rs,line=95,target=skootrs_lib::service::ecosystem) 105 | [2024-02-08T05:34:11.675Z] INFO: skootrs/16973 on Michaels-MBP-2.localdomain: Writing file README.md to ./ (file=skootrs-lib/src/service/facet.rs,line=115,target=skootrs_lib::service::facet) 106 | [2024-02-08T05:34:11.676Z] INFO: skootrs/16973 on Michaels-MBP-2.localdomain: Creating path "/tmp/skoot-test-bunyan/./" (file=skootrs-lib/src/service/source.rs,line=92,target=skootrs_lib::service::source) 107 | [2024-02-08T05:34:11.676Z] INFO: skootrs/16973 on Michaels-MBP-2.localdomain: Writing file LICENSE to ./ (file=skootrs-lib/src/service/facet.rs,line=115,target=skootrs_lib::service::facet) 108 | [2024-02-08T05:34:11.676Z] INFO: skootrs/16973 on Michaels-MBP-2.localdomain: Creating path "/tmp/skoot-test-bunyan/./" (file=skootrs-lib/src/service/source.rs,line=92,target=skootrs_lib::service::source) 109 | [2024-02-08T05:34:11.676Z] INFO: skootrs/16973 on Michaels-MBP-2.localdomain: Writing file .gitignore to ./ (file=skootrs-lib/src/service/facet.rs,line=115,target=skootrs_lib::service::facet) 110 | [2024-02-08T05:34:11.676Z] INFO: skootrs/16973 on Michaels-MBP-2.localdomain: Creating path "/tmp/skoot-test-bunyan/./" (file=skootrs-lib/src/service/source.rs,line=92,target=skootrs_lib::service::source) 111 | [2024-02-08T05:34:11.676Z] INFO: skootrs/16973 on Michaels-MBP-2.localdomain: Writing file SECURITY.md to ./ (file=skootrs-lib/src/service/facet.rs,line=115,target=skootrs_lib::service::facet) 112 | ``` 113 | 114 | ## Library docs: 115 | 116 | - https://docs.rs/skootrs-statestore/latest/skootrs_statestore/ 117 | - https://docs.rs/skootrs-model/latest/skootrs_model/ 118 | - https://docs.rs/skootrs-rest/latest/skootrs_rest/ 119 | - https://docs.rs/skootrs-lib/latest/skootrs_lib/ 120 | 121 | 122 | The initial talk given on Skootrs appears to not have been recorded but here are the locations of slides that include the reason why Skootrs is being built along with some architecture: 123 | 124 | - [OpenSSF Day Japan 2023](https://github.com/mlieberman85/talks/blob/91cf3bef51f7d277a744098863389e362920b4c8/2023-12-04-ossfday/presentation.pdf) 125 | - [NYU Guest Talk](https://github.com/mlieberman85/talks/blob/main/2024-01-30-skootrs/presentation.pdf) 126 | -------------------------------------------------------------------------------- /SECURITY-INSIGHTS.yml: -------------------------------------------------------------------------------- 1 | header: 2 | schema-version: 1.0.0 3 | expiration-date: '2024-12-04T10:10:09.000Z' 4 | last-updated: '2023-12-04' 5 | last-reviewed: '2023-12-04' 6 | project-url: https://github.com/kusaridev/kusari 7 | license: https://github.com/kusaridev/kusari/blob/main/LICENSE 8 | project-lifecycle: 9 | status: active 10 | bug-fixes-only: false 11 | core-maintainers: 12 | - github:mlieberman85 13 | contribution-policy: 14 | accepts-pull-requests: true 15 | accepts-automated-pull-requests: true 16 | security-contacts: 17 | - type: email 18 | value: mike@kusari.dev 19 | primary: true -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | Reporting Security Issues 2 | This project is currently pre-beta and should not currently be used in a production capacity or in any sensitive environments yet. 3 | 4 | However, this repo utilizes Github's private vulnerability reporting functionality. Follow the instructions here: https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability 5 | -------------------------------------------------------------------------------- /docs/design/project-state-store.md: -------------------------------------------------------------------------------- 1 | # Project State Store Design 2 | 3 | ## Summary 4 | 5 | Skootrs needs a way for knowing what actions it has done in creating and managing a project. This is required in order for Skootrs to be able to keep track of what projects it is managing for CRUD purposes. 6 | 7 | ## Motivation 8 | 9 | Currently, all state is stored in a local database on the host running Skootrs. This has multiple problems: 10 | - If something is changed out of band fro the Skootrs tool it can be difficult to know what has changed. 11 | - It is impossible to run Skootrs on a project from systems that weren't the one that originally created the project without exporting/importing from the database or copying the database to a new host. 12 | - It is impossible for someone to audit if someone else has run Skootrs on a project without access to this database. 13 | - It is impossible for multiple authorized parties to manage the same project through Skootrs 14 | 15 | ### Goals: 16 | - Provide mechanism to store and retrieve the state of a Skootrs project This would include: 17 | - High level metadata like name of project 18 | - List of facets created along with their associated metadata 19 | - Provide mechanism to give an identifier for a project (e.g. repo url) and have Skootrs be able to manage and audit that project. 20 | 21 | ### Non-Goals: 22 | - Provide secure mechanism for ensuring the Skootrs project state can't be modified by unauthorized actors or outside of the Skootrs tool. Eventually this will need to be considered. 23 | 24 | ## Proposal 25 | 26 | There are two main pieces to the proposal: 27 | - Store the state for a Skootrs project in the repo for the project itself 28 | - Store a local cache of references to projects the local Skootrs knows about 29 | 30 | ### In-repo State Store Design 31 | 32 | The main idea would be store the entirety of the state as a file inside the repo assuming it's not too big and doesn't change often. 33 | 34 | #### File 35 | 36 | The proposed solution is to just have a single file with the Skootrs project state inside of the repo. This file should just be called: `./skootrs` and kept in the root of the repo. 37 | 38 | #### Structure and Format 39 | 40 | The format of the state file should just be json with the structure being just the `InitializedProject` struct. 41 | 42 | ### Changes to be made 43 | 44 | #### Data Model 45 | 46 | Various `Initialized` structs will need to be updated to include information on: 47 | - Hash of the file(s) (if it's a file based facet) 48 | 49 | #### Services 50 | - `SourceBundleFacetService` needs to be updated to also take the hash of the file. 51 | - Some of the other services will need to be updated to support pulling/pushing 52 | 53 | 54 | ### Local Reference Cache Design 55 | 56 | This can just be a simple file called `.skootrscache` with just a list of repo URLs. 57 | 58 | ## Future 59 | 60 | There's a lot of things that can be done to improve on this in the future that could allow for things like: 61 | - Local caching of project state so you don't have to fetch it every time 62 | - Having some human readable mapping of project to project url 63 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "nightly" 3 | components = ["rustfmt"] 4 | targets = [ "x86_64-unknown-linux-gnu", "aarch64-unknown-linux-gnu", "x86_64-apple-darwin" ] -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import { } }: 2 | pkgs.mkShell rec { 3 | buildInputs = with pkgs; [ 4 | clang 5 | # Replace llvmPackages with llvmPackages_X, where X is the latest LLVM version (at the time of writing, 16) 6 | llvmPackages.bintools 7 | rustup 8 | bunyan-rs 9 | go 10 | maven 11 | pkg-config 12 | openssl 13 | ]; 14 | # TODO: Read toml toolchain file. 15 | RUSTC_VERSION = "nightly-x86_64-unknown-linux-gnu"; 16 | # https://github.com/rust-lang/rust-bindgen#environment-variables 17 | LIBCLANG_PATH = pkgs.lib.makeLibraryPath [ pkgs.llvmPackages_latest.libclang.lib ]; 18 | shellHook = '' 19 | export PATH=$PATH:''${CARGO_HOME:-~/.cargo}/bin 20 | export PATH=$PATH:''${RUSTUP_HOME:-~/.rustup}/toolchains/$RUSTC_VERSION-x86_64-unknown-linux-gnu/bin/ 21 | ''; 22 | # Add precompiled library to rustc search path 23 | RUSTFLAGS = (builtins.map (a: ''-L ${a}/lib'') [ 24 | # add libraries here (e.g. pkgs.libvmi) 25 | ]); 26 | # Add glibc, clang, glib and other headers to bindgen search path 27 | BINDGEN_EXTRA_CLANG_ARGS = 28 | # Includes with normal include path 29 | (builtins.map (a: ''-I"${a}/include"'') [ 30 | # add dev libraries here (e.g. pkgs.libvmi.dev) 31 | pkgs.glibc.dev 32 | ]) 33 | # Includes with special directory paths 34 | ++ [ 35 | ''-I"${pkgs.llvmPackages_latest.libclang.lib}/lib/clang/${pkgs.llvmPackages_latest.libclang.version}/include"'' 36 | ''-I"${pkgs.glib.dev}/include/glib-2.0"'' 37 | ''-I${pkgs.glib.out}/lib/glib-2.0/include/'' 38 | ]; 39 | 40 | } 41 | -------------------------------------------------------------------------------- /skootrs-bin/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "skootrs-bin" 3 | version = "0.1.0" 4 | edition = "2021" 5 | description = "This module is for the Skootrs CLI tool binary and helper functions." 6 | license = "Apache-2.0" 7 | repository = "https://github.com/kusaridev/skootrs" 8 | 9 | 10 | [[bin]] 11 | name = "skootrs" 12 | path = "src/main.rs" 13 | 14 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 15 | 16 | [dependencies] 17 | clap = { version = "4.5.4", features = ["derive"] } 18 | tracing = "0.1" 19 | skootrs-lib = { version = "0.1.0", path = "../skootrs-lib" } 20 | skootrs-rest = { version = "0.1.0", path = "../skootrs-rest" } 21 | skootrs-statestore = { version = "0.1.0", path = "../skootrs-statestore" } 22 | inquire = "0.6.2" 23 | octocrab = "0.32.0" 24 | tokio = { version = "1.34.0", features = ["full", "tracing", "macros", "rt-multi-thread"] } 25 | tracing-subscriber = { version = "0.3.18", features = ["registry", "env-filter"] } 26 | serde_json = "1.0.112" 27 | skootrs-model = { version = "0.1.0", path = "../skootrs-model" } 28 | opentelemetry-jaeger = { version = "0.20.0", features = ["rt-tokio-current-thread"] } 29 | tracing-opentelemetry = "0.22.0" 30 | tracing-bunyan-formatter = "0.3.9" 31 | opentelemetry = { version = "0.21.0" } 32 | opentelemetry_sdk = "0.21.2" 33 | serde_yaml = "0.9.32" 34 | reqwest = "0.11.24" 35 | base64 = "0.22.0" 36 | clio = { version = "0.3.5", features = ["clap", "clap-parse"] } 37 | serde = "1.0.197" 38 | strum = "0.26.2" 39 | 40 | [build-dependencies] 41 | clap_mangen = "0.2.20" 42 | -------------------------------------------------------------------------------- /skootrs-bin/README.md: -------------------------------------------------------------------------------- 1 | # Skootrs-bin 2 | 3 | This module is for the Skootrs CLI tool binary and helper functions. -------------------------------------------------------------------------------- /skootrs-bin/src/helpers.rs: -------------------------------------------------------------------------------- 1 | use inquire::Text; 2 | use octocrab::Page; 3 | use serde::Serialize; 4 | use skootrs_lib::service::{project::ProjectService, source::LocalSourceService}; 5 | use skootrs_model::skootrs::{ 6 | facet::InitializedFacet, Config, EcosystemInitializeParams, FacetGetParams, FacetMapKey, 7 | GithubRepoParams, GithubUser, GoParams, InitializedProject, ProjectArchiveParams, 8 | ProjectCreateParams, ProjectGetParams, ProjectOutput, ProjectOutputGetParams, 9 | ProjectOutputReference, ProjectOutputType, ProjectOutputsListParams, ProjectReleaseParam, 10 | ProjectUpdateParams, RepoCreateParams, SkootError, SourceInitializeParams, SupportedEcosystems, 11 | }; 12 | use std::{ 13 | collections::{HashMap, HashSet}, 14 | io::Write, 15 | str::FromStr, 16 | }; 17 | use strum::VariantNames; 18 | use tracing::debug; 19 | 20 | use skootrs_statestore::{ 21 | GitProjectStateStore, InMemoryProjectReferenceCache, ProjectReferenceCache, ProjectStateStore, 22 | }; 23 | 24 | /// Helper trait that lets me inline writing the result of a Skootrs function to a writer. 25 | pub trait HandleResponseOutput { 26 | #[must_use] 27 | fn handle_response_output(self, output_handler: W) -> Self; 28 | } 29 | 30 | impl HandleResponseOutput for Result 31 | where 32 | T: Serialize, 33 | { 34 | /// Handles a response that implements `Serialize`. 35 | /// This is useful for functions that return a response that needs to be printed out, logged, etc. to the user. 36 | /// 37 | /// # Errors 38 | /// 39 | /// Returns an error if the response can't be serialized to JSON or if the output can't be written to the output handler. 40 | /// Also returns an error if the function that returns the response returns an error. 41 | fn handle_response_output(self, mut output_handler: W) -> Self { 42 | match self { 43 | Ok(result) => { 44 | let serialized_result = serde_json::to_string_pretty(&result)?; 45 | writeln!(output_handler, "{serialized_result}")?; 46 | Ok(result) 47 | } 48 | Err(error) => Err(error), 49 | } 50 | } 51 | } 52 | 53 | pub struct Project; 54 | 55 | impl Project { 56 | /// Returns `Ok(())` if the project creation is successful, otherwise returns an error. 57 | /// 58 | /// Creates a new skootrs project by prompting the user for repository details and language selection. 59 | /// The project can be created for either Go or Maven ecosystems right now. 60 | /// The project is created in Github, cloned down, and then initialized along with any other security supporting 61 | /// tasks. If the `project_params` is not provided, the user will be prompted for the project details. 62 | /// 63 | /// # Errors 64 | /// 65 | /// Returns an error if the user is not authenticated with Github, or if the project can't be created 66 | /// for any other reason. 67 | pub async fn create<'a, T: ProjectService + ?Sized>( 68 | config: &Config, 69 | project_service: &'a T, 70 | project_params: Option, 71 | ) -> Result { 72 | let project_params = match project_params { 73 | Some(p) => p, 74 | None => Project::prompt_create(config).await?, 75 | }; 76 | 77 | let project = project_service.initialize(project_params).await?; 78 | let git_state_store = GitProjectStateStore { 79 | source: project.source.clone(), 80 | source_service: LocalSourceService {}, 81 | }; 82 | 83 | let mut local_cache = InMemoryProjectReferenceCache::load_or_create("./skootcache")?; 84 | git_state_store.create(project.clone()).await?; 85 | local_cache.set(project.repo.full_url()).await?; 86 | Ok(project) 87 | } 88 | 89 | async fn prompt_create(config: &Config) -> Result { 90 | let name = Text::new("The name of the repository").prompt()?; 91 | let description = Text::new("The description of the repository").prompt()?; 92 | let user = octocrab::instance().current().user().await?.login; 93 | let Page { items, .. } = octocrab::instance() 94 | .current() 95 | .list_org_memberships_for_authenticated_user() 96 | .send() 97 | .await?; 98 | let organization = inquire::Select::new( 99 | "Select an organization", 100 | items 101 | .iter() 102 | .map(|i| i.organization.login.as_str()) 103 | .chain(vec![user.as_str()]) 104 | .collect(), 105 | ) 106 | .prompt()?; 107 | let language = 108 | inquire::Select::new("Select a language", SupportedEcosystems::VARIANTS.to_vec()); 109 | 110 | let gh_org = match organization { 111 | x if x == user => GithubUser::User(x.to_string()), 112 | x => GithubUser::Organization(x.to_string()), 113 | }; 114 | 115 | let language_prompt = language.prompt()?; 116 | let ecosystem_params = match SupportedEcosystems::from_str(language_prompt)? { 117 | SupportedEcosystems::Go => EcosystemInitializeParams::Go(GoParams { 118 | name: name.clone(), 119 | host: format!("github.com/{organization}"), 120 | }), 121 | // TODO: Re-add Maven support. 122 | // TODO: Unclear if this is the right way to handle Maven group and artifact. 123 | /*SupportedEcosystems::Maven => EcosystemInitializeParams::Maven(MavenParams { 124 | group_id: format!("com.{organization}.{name}"), 125 | artifact_id: name.clone(), 126 | }),*/ 127 | }; 128 | 129 | let repo_params = RepoCreateParams::Github(GithubRepoParams { 130 | name: name.clone(), 131 | description, 132 | organization: gh_org, 133 | }); 134 | 135 | Ok(ProjectCreateParams { 136 | name: name.clone(), 137 | repo_params, 138 | ecosystem_params, 139 | source_params: SourceInitializeParams { 140 | parent_path: config.local_project_path.clone(), 141 | }, 142 | }) 143 | } 144 | 145 | /// Fetches the contents of an `InitializedProject` along with an interactive prompt. 146 | /// 147 | /// # Errors 148 | /// 149 | /// Returns an error if the project can't be fetched for some reason. 150 | pub async fn get<'a, T: ProjectService + ?Sized>( 151 | config: &Config, 152 | _project_service: &'a T, 153 | project_get_params: Option, 154 | ) -> Result { 155 | let mut cache = InMemoryProjectReferenceCache::load_or_create("./skootcache")?; 156 | let project_get_params = match project_get_params { 157 | Some(p) => p, 158 | None => Project::prompt_get(config).await?, 159 | }; 160 | let project = cache.get(project_get_params.project_url.clone()).await?; 161 | Ok(project) 162 | } 163 | 164 | async fn prompt_get(config: &Config) -> Result { 165 | let projects = Project::list(config).await?; 166 | let selected_project = 167 | inquire::Select::new("Select a project", projects.iter().collect()).prompt()?; 168 | Ok(ProjectGetParams { 169 | project_url: selected_project.clone(), 170 | }) 171 | } 172 | 173 | /// Updates an existing initialized project to include any updated facets. 174 | /// 175 | /// # Errors 176 | /// 177 | /// Returns an error if the project can't be updated for some reason. 178 | pub async fn update<'a, T: ProjectService + ?Sized>( 179 | config: &Config, 180 | project_service: &'a T, 181 | project_update_params: Option, 182 | ) -> Result { 183 | let mut cache = InMemoryProjectReferenceCache::load_or_create("./skootcache")?; 184 | let project_update_params = match project_update_params { 185 | Some(p) => p, 186 | None => Project::prompt_update(config, project_service).await?, 187 | }; 188 | let updated_project = project_service.update(project_update_params).await?; 189 | cache.set(updated_project.repo.full_url()).await?; 190 | Ok(updated_project) 191 | } 192 | 193 | async fn prompt_update<'a, T: ProjectService + ?Sized>( 194 | config: &Config, 195 | project_service: &'a T, 196 | ) -> Result { 197 | let initialized_project = Project::get(config, project_service, None).await?; 198 | Ok(ProjectUpdateParams { 199 | initialized_project, 200 | }) 201 | } 202 | 203 | /// Returns the list of projects that are stored in the cache. 204 | /// 205 | /// # Errors 206 | /// 207 | /// Returns an error if the cache can't be loaded or if the list of projects can't be fetched. 208 | pub async fn list(_config: &Config) -> Result, SkootError> { 209 | let cache = InMemoryProjectReferenceCache::load_or_create("./skootcache")?; 210 | let projects: HashSet = cache.list().await?; 211 | Ok(projects) 212 | } 213 | 214 | /// Archives a project by archiving the repository and removing it from the local cache. 215 | /// 216 | /// # Errors 217 | /// 218 | /// Returns an error if the project can't be archived or deleted from the cache. 219 | pub async fn archive<'a, T: ProjectService + ?Sized>( 220 | config: &Config, 221 | project_service: &'a T, 222 | project_archive_params: Option, 223 | ) -> Result<(), SkootError> { 224 | let project_archive_params = match project_archive_params { 225 | Some(p) => p, 226 | None => ProjectArchiveParams { 227 | initialized_project: Project::get(config, project_service, None).await?, 228 | }, 229 | }; 230 | let url = project_archive_params.initialized_project.repo.full_url(); 231 | project_service.archive(project_archive_params).await?; 232 | let mut local_cache = InMemoryProjectReferenceCache::load_or_create("./skootcache")?; 233 | local_cache.delete(url).await?; 234 | local_cache.save()?; 235 | Ok(()) 236 | } 237 | } 238 | 239 | pub struct Facet; 240 | 241 | impl Facet { 242 | /// Returns the contents of a facet. This includes things like source files or API bundles. 243 | /// 244 | /// # Errors 245 | /// 246 | /// Returns an error if the facet content or project can't be fetched for some reason. 247 | pub async fn get<'a, T: ProjectService + ?Sized>( 248 | config: &Config, 249 | project_service: &'a T, 250 | facet_get_params: Option, 251 | ) -> Result { 252 | let facet_get_params = if let Some(p) = facet_get_params { 253 | p 254 | } else { 255 | // let project = Project::get(config, project_service, None).await?; 256 | let project_get_params = Project::prompt_get(config).await?; 257 | let facet_map_keys = project_service 258 | .list_facets(project_get_params.clone()) 259 | .await?; 260 | let fmk = Facet::prompt_get(config, facet_map_keys.into_iter().collect())?; 261 | FacetGetParams { 262 | facet_map_key: fmk, 263 | project_get_params, 264 | } 265 | }; 266 | 267 | let facet_with_content = project_service 268 | .get_facet_with_content(facet_get_params) 269 | .await?; 270 | 271 | debug!("{:?}", facet_with_content); 272 | 273 | Ok(facet_with_content) 274 | } 275 | 276 | fn prompt_get( 277 | _config: &Config, 278 | facet_map_keys: Vec, 279 | ) -> Result { 280 | let facet_type = inquire::Select::new("Select a facet", facet_map_keys).prompt()?; 281 | 282 | Ok(facet_type) 283 | } 284 | 285 | /// Returns the list of facets for a project. This includes things like source files or API bundles. 286 | /// 287 | /// # Errors 288 | /// 289 | /// Returns an error if the project or list of facets can't be fetched for some reason. 290 | pub async fn list<'a, T: ProjectService + ?Sized>( 291 | config: &Config, 292 | project_service: &'a T, 293 | project_get_params: Option, 294 | ) -> Result, SkootError> { 295 | let project_get_params = match project_get_params { 296 | Some(p) => p, 297 | None => Project::prompt_get(config).await?, 298 | }; 299 | let facet_map_keys = project_service.list_facets(project_get_params).await?; 300 | Ok(facet_map_keys) 301 | } 302 | } 303 | 304 | pub struct Output; 305 | 306 | impl Output { 307 | /// Returns the content of a project output. This includes things like SBOMs or SLSA attestations. 308 | /// 309 | /// # Errors 310 | /// 311 | /// Returns an error if the project output can't be fetched from a project release. 312 | pub async fn get<'a, T: ProjectService + ?Sized>( 313 | config: &Config, 314 | project_service: &'a T, 315 | project_output_params: Option, 316 | ) -> Result { 317 | let project_output_params = match project_output_params { 318 | Some(p) => p, 319 | None => Output::prompt_output_get(config, project_service).await?, 320 | }; 321 | 322 | let output = project_service.output_get(project_output_params).await?; 323 | 324 | Ok(output) 325 | } 326 | 327 | /// Returns the list of project outputs for a project. This includes things like SBOMs or SLSA attestations. 328 | /// 329 | /// # Errors 330 | /// 331 | /// Returns an error if the project output list can't be fetched. 332 | pub async fn list<'a, T: ProjectService + ?Sized>( 333 | config: &Config, 334 | project_service: &'a T, 335 | project_outputs_list_params: Option, 336 | ) -> Result, SkootError> { 337 | let project_outputs_list_params = match project_outputs_list_params { 338 | Some(p) => p, 339 | None => ProjectOutputsListParams { 340 | initialized_project: Project::get(config, project_service, None).await?, 341 | release: ProjectReleaseParam::Latest, 342 | }, 343 | }; 344 | let output_list = project_service 345 | .outputs_list(project_outputs_list_params) 346 | .await?; 347 | Ok(output_list) 348 | } 349 | 350 | async fn prompt_output_get<'a, T: ProjectService + ?Sized>( 351 | config: &Config, 352 | project_service: &'a T, 353 | ) -> Result { 354 | let selected_project = Project::get(config, project_service, None).await?; 355 | let project_output_list_params = ProjectOutputsListParams { 356 | initialized_project: selected_project.clone(), 357 | // TODO: This should be a prompt. 358 | release: ProjectReleaseParam::Latest, 359 | }; 360 | let output_list = 361 | Output::list(config, project_service, Some(project_output_list_params)).await?; 362 | let type_output_map: HashMap> = output_list 363 | .iter() 364 | .map(|o| (o.output_type.to_string(), o.name.clone())) 365 | .fold( 366 | HashMap::new(), 367 | |mut acc: HashMap>, (key, value)| { 368 | acc.entry(key).or_default().push(value); 369 | acc 370 | }, 371 | ); 372 | let selected_output_type = inquire::Select::new( 373 | "Select an output type", 374 | type_output_map.keys().cloned().collect(), 375 | ) 376 | .prompt()?; 377 | let select_output_type_enum = ProjectOutputType::from_str(&selected_output_type)?; 378 | let selected_output = inquire::Select::new( 379 | "Select an output", 380 | type_output_map 381 | .get(&selected_output_type) 382 | .ok_or_else(|| SkootError::from("Failed to get output type"))? 383 | .clone(), 384 | ) 385 | .prompt()?; 386 | Ok(ProjectOutputGetParams { 387 | initialized_project: selected_project.clone(), 388 | project_output_type: select_output_type_enum, 389 | project_output: selected_output.clone(), 390 | // TODO: This should be selectable 391 | release: ProjectReleaseParam::Latest, 392 | }) 393 | } 394 | } 395 | -------------------------------------------------------------------------------- /skootrs-bin/src/main.rs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2024 The Skootrs Authors. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | //! Tool for creating and managing secure-by-default projects. 17 | //! 18 | //! This crate is for the binary that acts as the CLI which interacts 19 | //! with the other crates in the Skootrs project. 20 | //! 21 | //! The CLI is built using the `clap` crate, and the commands are 22 | //! using noun-verb syntax. So the commands are structured like: 23 | //! `skootrs `. For example, `skootrs project create`. 24 | //! 25 | //! The CLI if not given any arguments to a command will default to 26 | //! giving an interactive prompt to the user to fill in the required 27 | //! information. 28 | 29 | pub mod helpers; 30 | 31 | use std::io::stdout; 32 | 33 | use clap::{Parser, Subcommand}; 34 | use clio::Input; 35 | use skootrs_lib::service::ecosystem::LocalEcosystemService; 36 | use skootrs_lib::service::facet::LocalFacetService; 37 | use skootrs_lib::service::output::LocalOutputService; 38 | use skootrs_lib::service::project::LocalProjectService; 39 | use skootrs_lib::service::repo::LocalRepoService; 40 | use skootrs_lib::service::source::LocalSourceService; 41 | use skootrs_model::skootrs::SkootError; 42 | 43 | use helpers::{Facet, HandleResponseOutput, Output}; 44 | use opentelemetry::global; 45 | use opentelemetry_sdk::propagation::TraceContextPropagator; 46 | use serde::de::DeserializeOwned; 47 | use tracing::error; 48 | use tracing_bunyan_formatter::{BunyanFormattingLayer, JsonStorageLayer}; 49 | use tracing_subscriber::layer::SubscriberExt; 50 | use tracing_subscriber::{EnvFilter, Registry}; 51 | 52 | /// Skootrs is a CLI tool for creating and managing secure-by-default projects. 53 | /// The commands are using noun-verb syntax. So the commands are structured like: 54 | /// `skootrs `. For example, `skootrs project create`. 55 | /// 56 | /// The CLI if not given any arguments to a command will default to 57 | /// giving an interactive prompt to the user to fill in the required 58 | /// information. 59 | #[derive(Parser)] 60 | #[command(name = "skootrs")] 61 | #[command(bin_name = "skootrs")] 62 | enum SkootrsCli { 63 | /// Project commands. 64 | #[command(name = "project")] 65 | Project { 66 | #[clap(subcommand)] 67 | project: ProjectCommands, 68 | }, 69 | 70 | /// Facet commands. 71 | #[command(name = "facet")] 72 | Facet { 73 | #[clap(subcommand)] 74 | facet: FacetCommands, 75 | }, 76 | 77 | /// Output commands. 78 | #[command(name = "output")] 79 | Output { 80 | #[clap(subcommand)] 81 | output: OutputCommands, 82 | }, 83 | 84 | /// Daemon commands. 85 | #[command(name = "daemon")] 86 | Daemon { 87 | #[clap(subcommand)] 88 | daemon: DaemonCommands, 89 | }, 90 | } 91 | 92 | /// This is the enum for what nouns the `project` command can take. 93 | #[derive(Subcommand, Debug)] 94 | enum ProjectCommands { 95 | /// Create a new project. 96 | #[command(name = "create")] 97 | Create { 98 | /// This is an optional input parameter that can be used to pass in a file, pipe, url, or stdin. 99 | /// This is expected to be YAML or JSON. If it is not provided, the CLI will prompt the user for the input. 100 | #[clap(value_parser)] 101 | input: Option, 102 | }, 103 | /// Get the metadata for a particular project. 104 | #[command(name = "get")] 105 | Get { 106 | /// This is an optional input parameter that can be used to pass in a file, pipe, url, or stdin. 107 | /// This is expected to be YAML or JSON. If it is not provided, the CLI will prompt the user for the input. 108 | #[clap(value_parser)] 109 | input: Option, 110 | }, 111 | 112 | /// Update a project. 113 | #[command(name = "update")] 114 | Update { 115 | /// This is an optional input parameter that can be used to pass in a file, pipe, url, or stdin. 116 | /// This is expected to be YAML or JSON. If it is not provided, the CLI will prompt the user for the input. 117 | #[clap(value_parser)] 118 | input: Option, 119 | }, 120 | 121 | /// Archive a project. 122 | #[command(name = "archive")] 123 | Archive { 124 | /// This is an optional input parameter that can be used to pass in a file, pipe, url, or stdin. 125 | /// This is expected to be YAML or JSON. If it is not provided, the CLI will prompt the user for the input. 126 | #[clap(value_parser)] 127 | input: Option, 128 | }, 129 | 130 | /// List all the projects known to the local Skootrs 131 | #[command(name = "list")] 132 | List, 133 | } 134 | 135 | /// This is the enum for what nouns the `facet` command can take. 136 | #[derive(Subcommand, Debug)] 137 | enum FacetCommands { 138 | /// Get the data for a facet of a particular project. 139 | #[command(name = "get")] 140 | Get { 141 | /// This is an optional input parameter that can be used to pass in a file, pipe, url, or stdin. 142 | /// This is expected to be YAML or JSON. If it is not provided, the CLI will prompt the user for the input. 143 | #[clap(value_parser)] 144 | input: Option, 145 | }, 146 | /// List all the facets that belong to a particular project. 147 | #[command(name = "list")] 148 | List { 149 | /// This is an optional input parameter that can be used to pass in a file, pipe, url, or stdin. 150 | /// This is expected to be YAML or JSON. If it is not provided, the CLI will prompt the user for the input. 151 | #[clap(value_parser)] 152 | input: Option, 153 | }, 154 | } 155 | 156 | /// This is the enum for what nouns the `output` command can take. 157 | #[derive(Subcommand, Debug)] 158 | enum OutputCommands { 159 | /// Get the data for a release output of a particular project. 160 | #[command(name = "get")] 161 | Get { 162 | /// This is an optional input parameter that can be used to pass in a file, pipe, url, or stdin. 163 | /// This is expected to be YAML or JSON. If it is not provided, the CLI will prompt the user for the input. 164 | #[clap(value_parser)] 165 | input: Option, 166 | }, 167 | /// List all the release outputs that belong to a particular project. 168 | #[command(name = "list")] 169 | List { 170 | /// This is an optional input parameter that can be used to pass in a file, pipe, url, or stdin. 171 | /// This is expected to be YAML or JSON. If it is not provided, the CLI will prompt the user for the input. 172 | #[clap(value_parser)] 173 | input: Option, 174 | }, 175 | } 176 | 177 | /// This is the enum for what nouns the `daemon` command can take. 178 | #[derive(Subcommand, Debug)] 179 | enum DaemonCommands { 180 | /// Start the REST server. 181 | #[command(name = "start")] 182 | Start, 183 | } 184 | 185 | fn init_tracing() { 186 | let app_name = "skootrs"; 187 | 188 | // Start a new Jaeger trace pipeline. 189 | // Spans are exported in batch - recommended setup for a production application. 190 | global::set_text_map_propagator(TraceContextPropagator::new()); 191 | let tracer = opentelemetry_jaeger::new_agent_pipeline() 192 | .with_service_name(app_name) 193 | .install_simple() 194 | .expect("Failed to install OpenTelemetry tracer."); 195 | 196 | // Filter based on level - trace, debug, info, warn, error 197 | // Tunable via `RUST_LOG` env variable 198 | let env_filter = EnvFilter::try_from_default_env().unwrap_or(EnvFilter::new("info")); 199 | // Create a `tracing` layer using the Jaeger tracer 200 | let telemetry = tracing_opentelemetry::layer().with_tracer(tracer); 201 | // Create a `tracing` layer to emit spans as structured logs to stdout 202 | let formatting_layer = BunyanFormattingLayer::new(app_name.into(), std::io::stdout); 203 | // Combined them all together in a `tracing` subscriber 204 | let subscriber = Registry::default() 205 | .with(env_filter) 206 | .with(telemetry) 207 | .with(JsonStorageLayer) 208 | .with(formatting_layer); 209 | tracing::subscriber::set_global_default(subscriber) 210 | .expect("Failed to install `tracing` subscriber."); 211 | } 212 | 213 | /// TODO: This probably should be configurable in some way. 214 | fn init_project_service() -> LocalProjectService< 215 | LocalRepoService, 216 | LocalEcosystemService, 217 | LocalSourceService, 218 | LocalFacetService, 219 | LocalOutputService, 220 | > { 221 | LocalProjectService { 222 | repo_service: LocalRepoService {}, 223 | ecosystem_service: LocalEcosystemService {}, 224 | source_service: LocalSourceService {}, 225 | facet_service: LocalFacetService {}, 226 | output_service: LocalOutputService {}, 227 | } 228 | } 229 | 230 | fn parse_optional_input( 231 | input: Option, 232 | ) -> Result, SkootError> { 233 | match input { 234 | Some(input) => { 235 | // This should also support JSON since most modern YAML is a superset of JSON. 236 | // I don't care enough to support the edge cases right now. 237 | let params: T = serde_yaml::from_reader(input)?; 238 | Ok(Some(params)) 239 | } 240 | None => Ok(None), 241 | } 242 | } 243 | 244 | #[allow(clippy::too_many_lines)] 245 | #[tokio::main] 246 | async fn main() -> std::result::Result<(), SkootError> { 247 | init_tracing(); 248 | let cli = SkootrsCli::parse(); 249 | let o: octocrab::Octocrab = octocrab::Octocrab::builder() 250 | .personal_token( 251 | std::env::var("GITHUB_TOKEN").expect("GITHUB_TOKEN env var must be populated"), 252 | ) 253 | .build()?; 254 | octocrab::initialise(o); 255 | 256 | let project_service = init_project_service(); 257 | // TODO: This should only default when it can't pull a valid config from the environment. 258 | let config = skootrs_model::skootrs::Config::default(); 259 | 260 | match cli { 261 | SkootrsCli::Project { project } => match project { 262 | ProjectCommands::Create { input } => { 263 | let project_create_params = parse_optional_input(input)?; 264 | if let Err(ref error) = 265 | helpers::Project::create(&config, &project_service, project_create_params) 266 | .await 267 | .handle_response_output(stdout()) 268 | { 269 | error!(error = error.as_ref(), "Failed to create project"); 270 | } 271 | } 272 | ProjectCommands::Get { input } => { 273 | let project_get_params = parse_optional_input(input)?; 274 | if let Err(ref error) = 275 | helpers::Project::get(&config, &project_service, project_get_params) 276 | .await 277 | .handle_response_output(stdout()) 278 | { 279 | error!(error = error.as_ref(), "Failed to get project info"); 280 | } 281 | } 282 | ProjectCommands::Update { input } => { 283 | let project_update_params = parse_optional_input(input)?; 284 | if let Err(ref error) = 285 | helpers::Project::update(&config, &project_service, project_update_params) 286 | .await 287 | .handle_response_output(stdout()) 288 | { 289 | error!(error = error.as_ref(), "Failed to update project"); 290 | } 291 | } 292 | ProjectCommands::List => { 293 | if let Err(ref error) = helpers::Project::list(&config) 294 | .await 295 | .handle_response_output(stdout()) 296 | { 297 | error!(error = error.as_ref(), "Failed to list projects"); 298 | } 299 | } 300 | ProjectCommands::Archive { input } => { 301 | let project_archive_params = parse_optional_input(input)?; 302 | if let Err(ref error) = 303 | helpers::Project::archive(&config, &project_service, project_archive_params) 304 | .await 305 | { 306 | error!(error = error.as_ref(), "Failed to archive project"); 307 | } 308 | } 309 | }, 310 | SkootrsCli::Facet { facet } => match facet { 311 | FacetCommands::Get { input } => { 312 | let facet_get_params = parse_optional_input(input)?; 313 | if let Err(ref error) = Facet::get(&config, &project_service, facet_get_params) 314 | .await 315 | .handle_response_output(stdout()) 316 | { 317 | error!(error = error.as_ref(), "Failed to get facet"); 318 | } 319 | } 320 | FacetCommands::List { input } => { 321 | let project_get_params = parse_optional_input(input)?; 322 | if let Err(ref error) = Facet::list(&config, &project_service, project_get_params) 323 | .await 324 | .handle_response_output(stdout()) 325 | { 326 | error!(error = error.as_ref(), "Failed to list facets for project"); 327 | } 328 | } 329 | }, 330 | SkootrsCli::Output { output } => match output { 331 | OutputCommands::Get { input } => { 332 | let output_get_params = parse_optional_input(input)?; 333 | if let Err(ref error) = Output::get(&config, &project_service, output_get_params) 334 | .await 335 | .handle_response_output(stdout()) 336 | { 337 | error!(error = error.as_ref(), "Failed to get output"); 338 | } 339 | } 340 | OutputCommands::List { input } => { 341 | let output_list_params = parse_optional_input(input)?; 342 | if let Err(ref error) = Output::list(&config, &project_service, output_list_params) 343 | .await 344 | .handle_response_output(stdout()) 345 | { 346 | error!(error = error.as_ref(), "Failed to list outputs for project"); 347 | } 348 | } 349 | }, 350 | SkootrsCli::Daemon { daemon } => match daemon { 351 | DaemonCommands::Start => { 352 | tokio::task::spawn_blocking(|| { 353 | skootrs_rest::server::rest::run_server().expect("Failed to start REST Server"); 354 | }) 355 | .await 356 | .expect("REST Server Task Panicked"); 357 | } 358 | }, 359 | } 360 | 361 | Ok(()) 362 | } 363 | -------------------------------------------------------------------------------- /skootrs-lib/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "skootrs-lib" 3 | version = "0.1.0" 4 | edition = "2021" 5 | description = "This module contains most of the core functionality for Skootrs. It contains the code to interact with repo hosts, generate files, manage projects, etc." 6 | license = "Apache-2.0" 7 | repository = "https://github.com/kusaridev/skootrs" 8 | 9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 10 | 11 | [dependencies] 12 | octocrab = "0.33.3" 13 | serde_json = "1.0.112" 14 | serde_yaml = "0.9.32" 15 | serde = { version = "1.0.193", features = ["derive"] } 16 | utoipa = { version = "4.1.0" } 17 | chrono = { version = "0.4.31", features = ["serde"] } 18 | askama = "0.12.1" 19 | schemars = { version = "0.8.16", features = ["chrono", "url"] } 20 | tracing = "0.1" 21 | futures = "0.3.30" 22 | skootrs-model = { version = "0.1.0", path = "../skootrs-model" } 23 | sha2 = "0.10.8" 24 | url = "2.5.0" 25 | base64 = "0.22.0" 26 | reqwest = "0.12.3" 27 | 28 | [dev-dependencies] 29 | tempdir = "0.3.7" 30 | tokio = { version = "1.36.0", features = ["rt", "macros"] } 31 | -------------------------------------------------------------------------------- /skootrs-lib/README.md: -------------------------------------------------------------------------------- 1 | # Skootrs-lib 2 | 3 | This module contains most of the core functionality for Skootrs. It contains the code to interact with repo hosts, generate files, manage projects, etc. -------------------------------------------------------------------------------- /skootrs-lib/src/lib.rs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2023 The Skootrs Authors. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | //! This is the main entry point for core functionality in the Skootrs project. 17 | //! 18 | //! It contains the main traits and implementations for the core functionality of the project. 19 | //! This includes the creation of a project, managing of the project's repository, and the management 20 | //! of the project's source code. 21 | //! 22 | //! This crate also contains the concept of a facet, which is an abstraction for some piece of a project 23 | //! that is managed by Skootrs to provide a secure-by-default project. This includes things like sets of files 24 | //! in the source code or calls to the repository API. For example the SECURITY.md file or the API call that 25 | //! enables branch protection would be facets. 26 | #![feature(array_try_map)] 27 | 28 | pub mod service; 29 | -------------------------------------------------------------------------------- /skootrs-lib/src/service/ecosystem.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::module_name_repetitions)] 2 | 3 | use std::process::Command; 4 | 5 | use tracing::info; 6 | 7 | use skootrs_model::skootrs::{ 8 | EcosystemInitializeParams, GoParams, InitializedEcosystem, InitializedGo, InitializedMaven, 9 | InitializedSource, MavenParams, SkootError, 10 | }; 11 | 12 | /// The `EcosystemService` trait provides an interface for initializing and managing a project's ecosystem. 13 | /// An ecosystem is the language or packaging ecosystem that a project is built in, such as Maven or Go. 14 | pub trait EcosystemService { 15 | /// Initializes a project's ecosystem. This involves setting up the project's package or build system. 16 | /// For example `go mod init` for Go. 17 | /// 18 | /// # Errors 19 | /// 20 | /// Returns an error if the ecosystem can't be initialized. 21 | fn initialize( 22 | &self, 23 | params: EcosystemInitializeParams, 24 | source: InitializedSource, 25 | ) -> Result; 26 | } 27 | 28 | /// The `LocalEcosystemService` struct provides an implementation of the `EcosystemService` trait for initializing 29 | /// and managing a project's ecosystem on the local machine. 30 | #[derive(Debug)] 31 | pub struct LocalEcosystemService {} 32 | 33 | impl EcosystemService for LocalEcosystemService { 34 | fn initialize( 35 | &self, 36 | params: EcosystemInitializeParams, 37 | source: InitializedSource, 38 | ) -> Result { 39 | match params { 40 | EcosystemInitializeParams::Maven(m) => { 41 | LocalMavenEcosystemHandler::initialize(&source.path, &m)?; 42 | Ok(InitializedEcosystem::Maven(InitializedMaven { 43 | group_id: m.group_id, 44 | artifact_id: m.artifact_id, 45 | })) 46 | } 47 | EcosystemInitializeParams::Go(g) => { 48 | LocalGoEcosystemHandler::initialize(&source.path, &g)?; 49 | Ok(InitializedEcosystem::Go(InitializedGo { 50 | name: g.name, 51 | host: g.host, 52 | })) 53 | } 54 | } 55 | } 56 | } 57 | 58 | 59 | /// The `LocalMavenEcosystemHandler` struct represents a handler for initializing and managing a Maven 60 | /// project on the local machine. 61 | struct LocalMavenEcosystemHandler {} 62 | 63 | impl LocalMavenEcosystemHandler { 64 | /// Returns `Ok(())` if the Maven project initialization is successful, 65 | /// otherwise returns an error. 66 | fn initialize(path: &str, params: &MavenParams) -> Result<(), SkootError> { 67 | let output = Command::new("mvn") 68 | .arg("archetype:generate") 69 | .arg(format!("-DgroupId={}", params.group_id)) 70 | .arg(format!("-DartifactId={}", params.artifact_id)) 71 | .arg("-DarchetypeArtifactId=maven-archetype-quickstart") 72 | .arg("-DinteractiveMode=false") 73 | .current_dir(path) 74 | .output()?; 75 | if output.status.success() { 76 | info!("Initialized maven project for {}", params.artifact_id); 77 | Ok(()) 78 | } else { 79 | Err(Box::new(std::io::Error::new( 80 | std::io::ErrorKind::Other, 81 | "Failed to run mvn generate", 82 | ))) 83 | } 84 | } 85 | } 86 | 87 | /// The `LocalGoEcosystemHandler` struct represents a handler for initializing and managing a Go 88 | /// project on the local machine. 89 | struct LocalGoEcosystemHandler {} 90 | 91 | impl LocalGoEcosystemHandler { 92 | /// Returns an error if the initialization of a Go module at the specified 93 | /// path fails. 94 | /// 95 | /// # Arguments 96 | /// 97 | /// * `path` - The path where the Go module should be initialized. 98 | fn initialize(path: &str, params: &GoParams) -> Result<(), SkootError> { 99 | let output = Command::new("go") 100 | .arg("mod") 101 | .arg("init") 102 | .arg(params.module()) 103 | .current_dir(path) 104 | .output()?; 105 | if output.status.success() { 106 | info!("Initialized go module for {}", params.name); 107 | Ok(()) 108 | } else { 109 | Err(Box::new(std::io::Error::new( 110 | std::io::ErrorKind::Other, 111 | format!( 112 | "Failed to run go mod init: {}", 113 | String::from_utf8(output.stderr)? 114 | ), 115 | ))) 116 | } 117 | } 118 | } 119 | 120 | #[cfg(test)] 121 | mod tests { 122 | use super::*; 123 | use tempdir::TempDir; 124 | 125 | #[test] 126 | fn test_local_maven_ecosystem_handler_initialize_success() { 127 | let temp_dir = TempDir::new("test").unwrap(); 128 | let path = temp_dir.path().to_str().unwrap(); 129 | let params = MavenParams { 130 | group_id: "com.example".to_string(), 131 | artifact_id: "my-project".to_string(), 132 | }; 133 | 134 | let result = LocalMavenEcosystemHandler::initialize(path, ¶ms); 135 | 136 | assert!(result.is_ok()); 137 | } 138 | 139 | #[test] 140 | fn test_local_maven_ecosystem_handler_initialize_failure() { 141 | let temp_dir = TempDir::new("test").unwrap(); 142 | let path = temp_dir.path().to_str().unwrap(); 143 | let params = MavenParams { 144 | // Invalid group ID 145 | group_id: "".to_string(), 146 | artifact_id: "my-project".to_string(), 147 | }; 148 | 149 | let result = LocalMavenEcosystemHandler::initialize(path, ¶ms); 150 | 151 | assert!(result.is_err()); 152 | } 153 | 154 | #[test] 155 | fn test_local_go_ecosystem_handler_initialize_success() { 156 | let temp_dir = TempDir::new("test").unwrap(); 157 | let path = temp_dir.path().to_str().unwrap(); 158 | let params = GoParams { 159 | name: "my-project".to_string(), 160 | host: "github.com".to_string(), 161 | }; 162 | 163 | let result = LocalGoEcosystemHandler::initialize(path, ¶ms); 164 | 165 | assert!(result.is_ok()); 166 | } 167 | 168 | #[test] 169 | fn test_local_go_ecosystem_handler_initialize_failure() { 170 | let temp_dir = TempDir::new("test").unwrap(); 171 | let path = temp_dir.path().to_str().unwrap(); 172 | let params = GoParams { 173 | // Invalid project name 174 | name: "".to_string(), 175 | host: "github.com".to_string(), 176 | }; 177 | 178 | let result = LocalGoEcosystemHandler::initialize(path, ¶ms); 179 | 180 | assert!(result.is_err()); 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /skootrs-lib/src/service/mod.rs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2024 The Skootrs Authors. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | pub mod ecosystem; 17 | pub mod facet; 18 | pub mod output; 19 | pub mod project; 20 | pub mod repo; 21 | pub mod source; 22 | -------------------------------------------------------------------------------- /skootrs-lib/src/service/output.rs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2024 The Skootrs Authors. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | #![allow(clippy::module_name_repetitions)] 17 | 18 | use octocrab::models::repos::{Asset, Release}; 19 | use skootrs_model::skootrs::{ 20 | label::Label, ProjectOutput, ProjectOutputGetParams, ProjectOutputReference, ProjectOutputType, 21 | ProjectOutputsListParams, SkootError, 22 | }; 23 | pub trait OutputService { 24 | fn list( 25 | &self, 26 | params: ProjectOutputsListParams, 27 | ) -> impl std::future::Future, SkootError>> + Send; 28 | 29 | fn get( 30 | &self, 31 | _params: ProjectOutputGetParams, 32 | ) -> impl std::future::Future> + Send; 33 | } 34 | 35 | pub struct LocalOutputService; 36 | 37 | impl OutputService for LocalOutputService { 38 | fn list( 39 | &self, 40 | params: ProjectOutputsListParams, 41 | ) -> impl std::future::Future, SkootError>> + Send 42 | { 43 | match params.initialized_project.repo { 44 | skootrs_model::skootrs::InitializedRepo::Github(g) => { 45 | let github_params = GithubReleaseParams { 46 | owner: g.organization.get_name(), 47 | repo: g.name, 48 | tag: params.release.tag(), 49 | }; 50 | GithubReleaseHandler::outputs_list(github_params) 51 | } 52 | } 53 | } 54 | 55 | async fn get(&self, params: ProjectOutputGetParams) -> Result { 56 | match params.initialized_project.repo { 57 | skootrs_model::skootrs::InitializedRepo::Github(g) => { 58 | let github_params = GithubOutputGetParams { 59 | release: GithubReleaseHandler::get_release(GithubReleaseParams { 60 | owner: g.organization.get_name(), 61 | repo: g.name.clone(), 62 | tag: params.release.tag(), 63 | }) 64 | .await?, 65 | name: params.project_output, 66 | }; 67 | GithubReleaseHandler::get_output(github_params).await 68 | } 69 | } 70 | } 71 | } 72 | 73 | struct GithubReleaseHandler; 74 | impl GithubReleaseHandler { 75 | async fn outputs_list( 76 | params: GithubReleaseParams, 77 | ) -> Result, SkootError> { 78 | let release = Self::get_release(params).await?; 79 | 80 | let assets = release.assets; 81 | let references = assets 82 | .iter() 83 | .map(|asset| ProjectOutputReference { 84 | name: asset.name.clone(), 85 | output_type: Self::get_type(asset), 86 | labels: Self::get_labels(asset), 87 | }) 88 | .collect(); 89 | 90 | Ok(references) 91 | } 92 | 93 | async fn get_release(params: GithubReleaseParams) -> Result { 94 | match params.tag { 95 | Some(tag) => { 96 | octocrab::instance() 97 | .repos(params.owner, params.repo) 98 | .releases() 99 | .get_by_tag(tag.as_str()) 100 | .await 101 | } 102 | None => { 103 | octocrab::instance() 104 | .repos(params.owner, params.repo) 105 | .releases() 106 | .get_latest() 107 | .await 108 | } 109 | } 110 | } 111 | 112 | fn get_type(asset: &Asset) -> ProjectOutputType { 113 | // TODO: This matching probably isn't GitHub specific and can live somewhere more generalized. 114 | match asset.url { 115 | // Follows: https://github.com/ossf/sbom-everywhere/blob/main/reference/sbom_naming.md 116 | _ if asset.name.contains(".spdx.") => ProjectOutputType::SBOM, 117 | _ if asset.name.contains(".cdx.") => ProjectOutputType::SBOM, 118 | _ if asset.name.contains(".intoto.") => ProjectOutputType::InToto, 119 | // TODO: Add more types 120 | _ => ProjectOutputType::Unknown("Unknown".to_string()), 121 | } 122 | } 123 | 124 | fn get_labels(asset: &Asset) -> Vec