├── .gitignore ├── LICENSE ├── README.md ├── cmd ├── graph.go ├── root.go └── test.go ├── go.mod ├── go.sum ├── internal └── pkg │ └── test │ └── test.go ├── main.go ├── pkg ├── graph │ └── graph.go └── record │ ├── docdb.go │ ├── oci.go │ ├── record_provider.go │ └── rekor.go └── testdata ├── bar.json ├── baz.json ├── foo.json ├── qux.json └── waldo.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | -------------------------------------------------------------------------------- /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 [yyyy] [name of copyright owner] 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. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # scq 2 | 3 | This is a Supply Chain Query tool intended to query datastores containing attestations, SBOMs, and other supply chain metadata and build a graph that can be queried. 4 | 5 | This is currently a POC and is being tested by storing attestations in mongodb and thus relies on mongo db for testing. 6 | 7 | Right now the way you would test it out is: 8 | 9 | ``` 10 | go build 11 | ./scq test testdata/ 12 | cat testdata/foo.json | jq '.subject[0].digest.sha256' | xargs -I{} ./scq graph --hash {} | jq | less 13 | ``` 14 | 15 | The above commands will store the testdata into mongodb and then generate a graph based on the hash from the `foo.json` test attestation. It will recursively query the mongodb until it can't find any attestations to follow. -------------------------------------------------------------------------------- /cmd/graph.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2021 NAME HERE 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 | package cmd 17 | 18 | import ( 19 | "encoding/json" 20 | "fmt" 21 | "log" 22 | 23 | "github.com/mlieberman85/scq/pkg/graph" 24 | "github.com/mlieberman85/scq/pkg/record" 25 | "github.com/spf13/cobra" 26 | ) 27 | 28 | // graphCmd represents the graph command 29 | var graphCmd = &cobra.Command{ 30 | Use: "graph", 31 | Short: "Generates a supply chain graph based on attestations and metadata", 32 | Long: `TODO.`, 33 | Run: func(cmd *cobra.Command, args []string) { 34 | /*c, err := record.GetMongoClient("mongodb://localhost", "supplychain", "attestations") 35 | if err != nil { 36 | log.Fatal(err) 37 | }*/ 38 | c, err := record.GetRekorClient() 39 | if err != nil { 40 | log.Fatal(err) 41 | } 42 | 43 | // TODO: Make this configurable 44 | cs := []record.RecordClient{c} 45 | 46 | scg := graph.SupplyChainGraph{ 47 | Nodes: make(map[string]*record.Record), 48 | Edges: make(map[string]map[string]struct{}), 49 | RecordManager: &record.Manager{ 50 | Opts: record.ManagerOpts{ 51 | IsTest: true, 52 | }, 53 | Clients: cs, 54 | }, 55 | } 56 | 57 | hash, err := cmd.Flags().GetString("hash") 58 | if err != nil { 59 | log.Fatal(err) 60 | } 61 | 62 | err = scg.GenerateFromHash(hash) 63 | if err != nil { 64 | log.Fatal(err) 65 | } 66 | 67 | j, err := json.Marshal(scg) 68 | if err != nil { 69 | log.Fatal(err) 70 | } 71 | fmt.Println(string(j)) 72 | 73 | }, 74 | } 75 | 76 | func init() { 77 | rootCmd.AddCommand(graphCmd) 78 | graphCmd.Flags().String("hash", "g", "Hash of the artifact you want generate a graph for") 79 | } 80 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2021 NAME HERE 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 | package cmd 17 | 18 | import ( 19 | "fmt" 20 | "os" 21 | 22 | "github.com/spf13/cobra" 23 | 24 | "github.com/spf13/viper" 25 | ) 26 | 27 | var cfgFile string 28 | 29 | // rootCmd represents the base command when called without any subcommands 30 | var rootCmd = &cobra.Command{ 31 | Use: "scq", 32 | Short: "Supply Chain Query Tool", 33 | Long: `scq is intended to be a query tool that fetches supply chain records and recursively follows 34 | records in those records in order to build a supply chain graph in order to audit and query your 35 | supply chain.`, 36 | // Uncomment the following line if your bare application 37 | // has an action associated with it: 38 | // Run: func(cmd *cobra.Command, args []string) { }, 39 | } 40 | 41 | // Execute adds all child commands to the root command and sets flags appropriately. 42 | // This is called by main.main(). It only needs to happen once to the rootCmd. 43 | func Execute() { 44 | cobra.CheckErr(rootCmd.Execute()) 45 | } 46 | 47 | func init() { 48 | cobra.OnInitialize(initConfig) 49 | 50 | // Here you will define your flags and configuration settings. 51 | // Cobra supports persistent flags, which, if defined here, 52 | // will be global for your application. 53 | 54 | rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.scq.yaml)") 55 | 56 | // Cobra also supports local flags, which will only run 57 | // when this action is called directly. 58 | rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") 59 | } 60 | 61 | // initConfig reads in config file and ENV variables if set. 62 | func initConfig() { 63 | if cfgFile != "" { 64 | // Use config file from the flag. 65 | viper.SetConfigFile(cfgFile) 66 | } else { 67 | // Find home directory. 68 | home, err := os.UserHomeDir() 69 | cobra.CheckErr(err) 70 | 71 | // Search config in home directory with name ".scq" (without extension). 72 | viper.AddConfigPath(home) 73 | viper.SetConfigType("yaml") 74 | viper.SetConfigName(".scq") 75 | } 76 | 77 | viper.AutomaticEnv() // read in environment variables that match 78 | 79 | // If a config file is found, read it in. 80 | if err := viper.ReadInConfig(); err == nil { 81 | fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed()) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /cmd/test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2021 NAME HERE 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 | package cmd 17 | 18 | import ( 19 | "github.com/mlieberman85/scq/internal/pkg/test" 20 | 21 | "github.com/spf13/cobra" 22 | ) 23 | 24 | // testCmd represents the test command 25 | var testCmd = &cobra.Command{ 26 | Use: "test", 27 | Short: "Uploads test data to a mongodb", 28 | Long: `TODO.`, 29 | Args: cobra.ExactArgs(1), 30 | Run: func(cmd *cobra.Command, args []string) { 31 | test.UploadTestData(args[0]) 32 | }, 33 | } 34 | 35 | func init() { 36 | rootCmd.AddCommand(testCmd) 37 | } 38 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mlieberman85/scq 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/go-openapi/runtime v0.21.0 7 | github.com/google/go-containerregistry v0.7.1-0.20211203164431-c75901cce627 8 | github.com/in-toto/in-toto-golang v0.3.4-0.20211211042327-af1f9fb822bf 9 | github.com/sigstore/cosign v1.4.2-0.20211228190612-591601cad3ea 10 | github.com/sigstore/rekor v0.4.0 11 | github.com/spf13/cobra v1.3.0 12 | github.com/spf13/viper v1.10.1 13 | go.mongodb.org/mongo-driver v1.8.1 14 | gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 15 | ) 16 | 17 | require ( 18 | cloud.google.com/go v0.99.0 // indirect 19 | cloud.google.com/go/storage v1.18.2 // indirect 20 | github.com/PuerkitoBio/purell v1.1.1 // indirect 21 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect 22 | github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect 23 | github.com/blang/semver v3.5.1+incompatible // indirect 24 | github.com/census-instrumentation/opencensus-proto v0.3.0 // indirect 25 | github.com/cespare/xxhash/v2 v2.1.2 // indirect 26 | github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4 // indirect 27 | github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490 // indirect 28 | github.com/containerd/stargz-snapshotter/estargz v0.10.1 // indirect 29 | github.com/cyberphone/json-canonicalization v0.0.0-20210823021906-dc406ceaf94b // indirect 30 | github.com/docker/cli v20.10.11+incompatible // indirect 31 | github.com/docker/distribution v2.7.1+incompatible // indirect 32 | github.com/docker/docker v20.10.11+incompatible // indirect 33 | github.com/docker/docker-credential-helpers v0.6.4 // indirect 34 | github.com/envoyproxy/go-control-plane v0.10.1 // indirect 35 | github.com/envoyproxy/protoc-gen-validate v0.6.2 // indirect 36 | github.com/fsnotify/fsnotify v1.5.1 // indirect 37 | github.com/ghodss/yaml v1.0.0 // indirect 38 | github.com/go-chi/chi v4.1.2+incompatible // indirect 39 | github.com/go-openapi/analysis v0.20.1 // indirect 40 | github.com/go-openapi/errors v0.20.1 // indirect 41 | github.com/go-openapi/jsonpointer v0.19.5 // indirect 42 | github.com/go-openapi/jsonreference v0.19.6 // indirect 43 | github.com/go-openapi/loads v0.21.0 // indirect 44 | github.com/go-openapi/spec v0.20.4 // indirect 45 | github.com/go-openapi/strfmt v0.21.1 // indirect 46 | github.com/go-openapi/swag v0.19.15 // indirect 47 | github.com/go-openapi/validate v0.20.3 // indirect 48 | github.com/go-playground/locales v0.14.0 // indirect 49 | github.com/go-playground/universal-translator v0.18.0 // indirect 50 | github.com/go-playground/validator/v10 v10.9.0 // indirect 51 | github.com/go-stack/stack v1.8.0 // indirect 52 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 53 | github.com/golang/protobuf v1.5.2 // indirect 54 | github.com/golang/snappy v0.0.4 // indirect 55 | github.com/google/go-cmp v0.5.6 // indirect 56 | github.com/google/trillian v1.4.0 // indirect 57 | github.com/googleapis/gax-go/v2 v2.1.1 // indirect 58 | github.com/hashicorp/hcl v1.0.0 // indirect 59 | github.com/inconshreveable/mousetrap v1.0.0 // indirect 60 | github.com/jedisct1/go-minisign v0.0.0-20210703085342-c1f07ee84431 // indirect 61 | github.com/josharian/intern v1.0.0 // indirect 62 | github.com/klauspost/compress v1.13.6 // indirect 63 | github.com/leodido/go-urn v1.2.1 // indirect 64 | github.com/magiconair/properties v1.8.5 // indirect 65 | github.com/mailru/easyjson v0.7.7 // indirect 66 | github.com/mitchellh/go-homedir v1.1.0 // indirect 67 | github.com/mitchellh/mapstructure v1.4.3 // indirect 68 | github.com/oklog/ulid v1.3.1 // indirect 69 | github.com/opencontainers/go-digest v1.0.0 // indirect 70 | github.com/opencontainers/image-spec v1.0.2-0.20211117181255-693428a734f5 // indirect 71 | github.com/opentracing/opentracing-go v1.2.0 // indirect 72 | github.com/pelletier/go-toml v1.9.4 // indirect 73 | github.com/pkg/errors v0.9.1 // indirect 74 | github.com/sassoftware/relic v0.0.0-20210427151427-dfb082b79b74 // indirect 75 | github.com/secure-systems-lab/go-securesystemslib v0.3.0 // indirect 76 | github.com/shibumi/go-pathspec v1.3.0 // indirect 77 | github.com/sigstore/sigstore v1.0.2-0.20211223185235-75bc699f921d // indirect 78 | github.com/sirupsen/logrus v1.8.1 // indirect 79 | github.com/spf13/afero v1.6.0 // indirect 80 | github.com/spf13/cast v1.4.1 // indirect 81 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 82 | github.com/spf13/pflag v1.0.5 // indirect 83 | github.com/subosito/gotenv v1.2.0 // indirect 84 | github.com/syndtr/goleveldb v1.0.0 // indirect 85 | github.com/tent/canonical-json-go v0.0.0-20130607151641-96e4ba3a7613 // indirect 86 | github.com/theupdateframework/go-tuf v0.0.0-20211213174152-470b5ab00139 // indirect 87 | github.com/vbatts/tar-split v0.11.2 // indirect 88 | github.com/xdg-go/pbkdf2 v1.0.0 // indirect 89 | github.com/xdg-go/scram v1.0.2 // indirect 90 | github.com/xdg-go/stringprep v1.0.2 // indirect 91 | github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect 92 | go.opencensus.io v0.23.0 // indirect 93 | go.uber.org/atomic v1.9.0 // indirect 94 | go.uber.org/multierr v1.7.0 // indirect 95 | go.uber.org/zap v1.19.1 // indirect 96 | golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b // indirect 97 | golang.org/x/mod v0.5.1 // indirect 98 | golang.org/x/net v0.0.0-20211209124913-491a49abca63 // indirect 99 | golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect 100 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect 101 | golang.org/x/sys v0.0.0-20211210111614-af8b64212486 // indirect 102 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect 103 | golang.org/x/text v0.3.7 // indirect 104 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect 105 | google.golang.org/api v0.63.0 // indirect 106 | google.golang.org/appengine v1.6.7 // indirect 107 | google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa // indirect 108 | google.golang.org/grpc v1.43.0 // indirect 109 | google.golang.org/protobuf v1.27.1 // indirect 110 | gopkg.in/ini.v1 v1.66.2 // indirect 111 | gopkg.in/yaml.v2 v2.4.0 // indirect 112 | knative.dev/pkg v0.0.0-20211215065729-552319d4f55b // indirect 113 | ) 114 | -------------------------------------------------------------------------------- /internal/pkg/test/test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "time" 9 | 10 | "go.mongodb.org/mongo-driver/mongo" 11 | "go.mongodb.org/mongo-driver/mongo/options" 12 | "gopkg.in/mgo.v2/bson" 13 | b "gopkg.in/mgo.v2/bson" 14 | ) 15 | 16 | func UploadTestData(testDataDir string) error { 17 | client, err := mongo.NewClient(options.Client().ApplyURI("mongodb://localhost")) 18 | if err != nil { 19 | return err 20 | } 21 | ctx, _ := context.WithTimeout(context.Background(), 10*time.Second) 22 | err = client.Connect(ctx) 23 | if err != nil { 24 | return err 25 | } 26 | defer client.Disconnect(ctx) 27 | 28 | scDatabase := client.Database(("supplychain")) 29 | err = scDatabase.Collection("attestations").Drop(ctx) 30 | if err != nil { 31 | return err 32 | } 33 | attestationCollection := scDatabase.Collection("attestations") 34 | mod := mongo.IndexModel{ 35 | Keys: bson.M{ 36 | "subject.digest.sha256": 1, 37 | }, 38 | Options: nil, 39 | } 40 | 41 | _, err = attestationCollection.Indexes().CreateOne(ctx, mod) 42 | if err != nil { 43 | return err 44 | } 45 | 46 | var testAttestations []interface{} 47 | files, err := ioutil.ReadDir(testDataDir) 48 | if err != nil { 49 | return err 50 | } 51 | 52 | for _, file := range files { 53 | if !file.IsDir() { 54 | data, err := os.ReadFile(fmt.Sprintf("%s/%s", testDataDir, file.Name())) 55 | if err != nil { 56 | return err 57 | } 58 | 59 | var bdoc bson.M 60 | err = b.UnmarshalJSON(data, &bdoc) 61 | if err != nil { 62 | return err 63 | } 64 | 65 | testAttestations = append(testAttestations, bdoc) 66 | } 67 | } 68 | attestationCollection.InsertMany(ctx, testAttestations) 69 | 70 | return nil 71 | } 72 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/mlieberman85/scq/cmd" 4 | 5 | func main() { 6 | cmd.Execute() 7 | } 8 | -------------------------------------------------------------------------------- /pkg/graph/graph.go: -------------------------------------------------------------------------------- 1 | package graph 2 | 3 | import ( 4 | "github.com/mlieberman85/scq/pkg/record" 5 | ) 6 | 7 | type SupplyChainGraph struct { 8 | Nodes map[string]*record.Record 9 | Edges map[string]map[string]struct{} // Should this be a set of some sort? 10 | RecordManager *record.Manager 11 | } 12 | 13 | func (scg *SupplyChainGraph) GenerateFromHash(hash string) error { 14 | if _, ok := scg.Edges[hash]; !ok { 15 | scg.Edges[hash] = make(map[string]struct{}) 16 | } 17 | 18 | r, err := scg.RecordManager.GetRecord(hash) 19 | if _, ok := scg.Nodes[hash]; !ok { 20 | // TODO: How do we want to handle mutliple data all associated with a single hash? 21 | scg.Nodes[hash] = &r 22 | } 23 | if err != nil { 24 | return err 25 | } 26 | 27 | for _, m := range r.GetMaterials() { 28 | // Check if seen hash before in these edges. 29 | if _, ok := scg.Edges[hash][m.Digest]; !ok { 30 | scg.Edges[hash][m.Digest] = struct{}{} 31 | // Check if processed hash before 32 | if _, node := scg.Nodes[m.Digest]; !node { 33 | err = scg.GenerateFromHash(m.Digest) 34 | if err != nil { 35 | // TODO: Ignore only errors where no matching entries are found 36 | } 37 | } 38 | } 39 | } 40 | 41 | return nil 42 | } 43 | -------------------------------------------------------------------------------- /pkg/record/docdb.go: -------------------------------------------------------------------------------- 1 | package record 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | 7 | "github.com/in-toto/in-toto-golang/in_toto" 8 | "go.mongodb.org/mongo-driver/mongo" 9 | "go.mongodb.org/mongo-driver/mongo/options" 10 | "gopkg.in/mgo.v2/bson" 11 | ) 12 | 13 | type DocDBRecord struct { 14 | Attestation Attestation 15 | } 16 | 17 | func (d *DocDBRecord) GetData() interface{} { 18 | return d.Attestation 19 | } 20 | 21 | func (d *DocDBRecord) GetMaterials() []Material { 22 | var ms []Material 23 | for _, predicateMaterial := range d.Attestation.(*in_toto.ProvenanceStatement).Predicate.Materials { 24 | // For now just get the first hash in the map. 25 | var d string 26 | for _, digest := range predicateMaterial.Digest { 27 | d = digest 28 | break 29 | } 30 | 31 | m := Material{ 32 | MediaType: "todo", 33 | Digest: d, 34 | Uri: predicateMaterial.URI, 35 | } 36 | ms = append(ms, m) 37 | } 38 | 39 | return ms 40 | } 41 | 42 | func (d *DocDBRecord) GetType() string { 43 | return "docdbrecord" 44 | } 45 | 46 | type MongoClient struct { 47 | ctx context.Context 48 | collection mongo.Collection 49 | client *mongo.Client 50 | } 51 | 52 | func GetMongoClient(uri string, db string, collection string) (*MongoClient, error) { 53 | client, err := mongo.NewClient(options.Client().ApplyURI(uri)) // mongodb://localhost 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | err = client.Connect(context.Background()) 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | err = client.Ping(context.Background(), nil) 64 | if err != nil { 65 | return nil, err 66 | } 67 | scDatabase := client.Database((db)) // supplychain 68 | attestationsCollection := scDatabase.Collection(collection) // attestations 69 | 70 | return &MongoClient{ 71 | ctx: context.Background(), 72 | collection: *attestationsCollection, 73 | client: client, 74 | }, nil 75 | } 76 | 77 | func (mc *MongoClient) GetRecord(hash string) (Record, error) { 78 | c, err := mc.collection.Find(mc.ctx, bson.M{"subject.digest.sha256": hash}, options.Find().SetProjection(bson.M{"_id": 0})) 79 | if err != nil { 80 | return nil, err 81 | } 82 | 83 | var results []bson.M 84 | err = c.All(mc.ctx, &results) 85 | if err != nil { 86 | return nil, err 87 | } 88 | 89 | r := DocDBRecord{ 90 | Attestation: nil, 91 | } 92 | 93 | if len(results) > 0 { 94 | d := results[0] 95 | m, err := bson.Marshal(d) 96 | if err != nil { 97 | return nil, err 98 | } 99 | 100 | var p *in_toto.ProvenanceStatement 101 | var p2 interface{} 102 | 103 | // FIXME: For some reason bson doesn't unmarshal the statementheader correctly 104 | // But json marshalling/unmarshalling does 105 | /*err = bson.Unmarshal(m, &p) 106 | if err != nil { 107 | return nil, err 108 | }*/ 109 | err = bson.Unmarshal(m, &p2) 110 | if err != nil { 111 | return nil, err 112 | } 113 | j, err := json.Marshal(p2) 114 | if err != nil { 115 | return nil, err 116 | } 117 | err = json.Unmarshal(j, &p) 118 | r.Attestation = p 119 | } 120 | 121 | return &r, nil 122 | } 123 | -------------------------------------------------------------------------------- /pkg/record/oci.go: -------------------------------------------------------------------------------- 1 | package record 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | 7 | "github.com/google/go-containerregistry/pkg/name" 8 | "github.com/in-toto/in-toto-golang/in_toto" 9 | "github.com/sigstore/cosign/pkg/cosign" 10 | ) 11 | 12 | // WIP 13 | 14 | // Uses cosign to do most of the heavy lifting. 15 | type OCIRecord struct { 16 | Attestation in_toto.ProvenanceStatement 17 | } 18 | 19 | func (o *OCIRecord) GetType() string { 20 | return "slsa" 21 | } 22 | 23 | func (o *OCIRecord) GetData() interface{} { 24 | return o.Attestation 25 | } 26 | 27 | func (o *OCIRecord) GetMaterials() []Material { 28 | // TODO: Make wrapper for in_toto predicate types as that's going to be a common one 29 | var ms []Material 30 | for _, predicateMaterial := range o.Attestation.Predicate.Materials { 31 | // For now just get the first hash in the map. 32 | var d string 33 | for _, digest := range predicateMaterial.Digest { 34 | d = digest 35 | break 36 | } 37 | 38 | m := Material{ 39 | MediaType: "todo", 40 | Digest: d, 41 | Uri: predicateMaterial.URI, 42 | } 43 | ms = append(ms, m) 44 | } 45 | 46 | return ms 47 | } 48 | 49 | // TODO: Set up trusted keys for records 50 | // This for now is intended to purely represent records related to a particular image 51 | type OCIClient struct { 52 | //trustedKeyRefs []string 53 | imageURI string 54 | } 55 | 56 | func (o *OCIClient) GetRecord(hash string) (Record, error) { 57 | ctx := context.Background() 58 | 59 | d, err := name.NewDigest(o.imageURI) 60 | if err != nil { 61 | return nil, err 62 | } 63 | 64 | as, err := cosign.FetchAttestationsForReference(ctx, d) 65 | if err != nil { 66 | return nil, err 67 | } 68 | 69 | // TODO: What to do when multiple attestations are found? 70 | if len(as) == 0 { 71 | return nil, nil 72 | } 73 | 74 | var provenance in_toto.ProvenanceStatement 75 | // TODO: This currently doesn't handle anything but slsa provenance 76 | err = json.Unmarshal([]byte(as[0].PayLoad), &provenance) 77 | if err != nil { 78 | return nil, err 79 | } 80 | 81 | r := OCIRecord{ 82 | Attestation: provenance, 83 | } 84 | return &r, nil 85 | } 86 | -------------------------------------------------------------------------------- /pkg/record/record_provider.go: -------------------------------------------------------------------------------- 1 | package record 2 | 3 | /* 4 | Some basic thoughts about the record provider: 5 | * Should try to first follow URI references in records, followed by a prioritized list of 6 | clients in a config file. 7 | */ 8 | 9 | type Attestation interface { 10 | } 11 | 12 | type Body interface { 13 | } 14 | 15 | // TODO: This shoould probably just use something like OCI Content descriptors 16 | type Material struct { 17 | MediaType string 18 | Digest string 19 | Uri string 20 | } 21 | 22 | type Record interface { 23 | GetType() string 24 | GetMaterials() []Material 25 | GetData() interface{} 26 | } 27 | 28 | type ManagerOpts struct { 29 | IsTest bool 30 | } 31 | 32 | // TODO: Should probably create some creator functions for this 33 | type Manager struct { 34 | Opts ManagerOpts 35 | // TODO: There should be a way to create a way to prioritize different backends for different types of records 36 | Clients []RecordClient 37 | } 38 | 39 | // TODO: This client is intended to follow URIs found in records. This will require some consensus in the community 40 | type URIFollowerClient struct{} 41 | 42 | type RecordClient interface { 43 | GetRecord(hash string) (Record, error) 44 | } 45 | 46 | func (m *Manager) GetRecord(hash string) (Record, error) { 47 | // TODO: Figure out if we fetch all records in all places or just the first record found 48 | for _, c := range m.Clients { 49 | r, err := c.GetRecord(hash) 50 | if err != nil { 51 | return nil, err 52 | } 53 | // Record found so return it 54 | if r != nil { 55 | return r, nil 56 | } 57 | // Record not found so go to the next client 58 | } 59 | // Record not found 60 | return nil, nil 61 | } 62 | -------------------------------------------------------------------------------- /pkg/record/rekor.go: -------------------------------------------------------------------------------- 1 | package record 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "encoding/json" 7 | "fmt" 8 | "log" 9 | "time" 10 | 11 | "github.com/go-openapi/runtime" 12 | "github.com/in-toto/in-toto-golang/in_toto" 13 | rekor_client "github.com/sigstore/rekor/pkg/client" 14 | "github.com/sigstore/rekor/pkg/generated/client" 15 | "github.com/sigstore/rekor/pkg/generated/client/entries" 16 | "github.com/sigstore/rekor/pkg/generated/client/index" 17 | "github.com/sigstore/rekor/pkg/generated/models" 18 | ) 19 | 20 | // TODO: This needs to be rewritten. A lot of this is more or less copied from various sigstore tools 21 | // since the Rekor API is mostly generated openapi code. 22 | 23 | const rekor_server = "https://rekor.sigstore.dev" 24 | 25 | type RekorRecord struct { 26 | Body Body 27 | Attestation Attestation 28 | } 29 | 30 | type RekorClient struct { 31 | client *client.Rekor 32 | } 33 | 34 | // TODO: Make customizable 35 | func GetRekorClient() (*RekorClient, error) { 36 | rc, err := rekor_client.GetRekorClient(rekor_server) 37 | if err != nil { 38 | return nil, err 39 | } 40 | c := RekorClient{ 41 | rc, 42 | } 43 | 44 | return &c, nil 45 | } 46 | 47 | func (rc *RekorClient) GetRecord(hash string) (Record, error) { 48 | UUIDs, err := searchRekor(rc.client, hash) 49 | if err != nil { 50 | return nil, err 51 | } 52 | 53 | for _, u := range UUIDs { 54 | entry, err := getEntry(rc.client, u) 55 | if err != nil { 56 | log.Fatal(err) 57 | } 58 | // TODO: This mean no attestation was found in rekor which should not be the case. 59 | // Need to investigate why this is happening. Seems like a rekor issue 60 | if entry.Attestation.Data != nil { 61 | record, err := parseEntry(entry) 62 | if err != nil { 63 | log.Fatal(err) 64 | } 65 | return record, nil 66 | } else { 67 | continue 68 | } 69 | } 70 | return nil, fmt.Errorf("Unreachable!") 71 | } 72 | 73 | func (r *RekorRecord) GetData() interface{} { 74 | return r 75 | } 76 | 77 | func (r *RekorRecord) GetType() string { 78 | return "rekor" 79 | } 80 | 81 | func (r *RekorRecord) GetMaterials() []Material { 82 | var ms []Material 83 | for _, predicateMaterial := range r.Attestation.(*in_toto.ProvenanceStatement).Predicate.Materials { 84 | // For now just get the first value in the map. 85 | var d string 86 | for _, digest := range predicateMaterial.Digest { 87 | d = digest 88 | break 89 | } 90 | m := Material{ 91 | MediaType: "todo", // TODO: Get folks to use OCI in their attestations 92 | Digest: d, 93 | } 94 | ms = append(ms, m) 95 | } 96 | 97 | return ms 98 | } 99 | 100 | func parseStatement(p string) (*in_toto.ProvenanceStatement, error) { 101 | ps := in_toto.ProvenanceStatement{} 102 | payload, err := base64.StdEncoding.DecodeString(p) 103 | if err != nil { 104 | return nil, err 105 | } 106 | if err := json.Unmarshal(payload, &ps); err != nil { 107 | return nil, err 108 | } 109 | return &ps, nil 110 | } 111 | 112 | func searchRekor(rekorClient *client.Rekor, sha string) ([]string, error) { 113 | UUIDs := make(map[string]struct{}) 114 | 115 | searchIndexParams := index.NewSearchIndexParams() 116 | searchIndexParams.SetTimeout(time.Minute) // TODO: Make configurable 117 | searchIndexParams.Query = &models.SearchIndex{} 118 | 119 | searchIndexParams.Query.Hash = fmt.Sprintf("%s:%s", "sha256", sha) 120 | 121 | resp, err := rekorClient.Index.SearchIndex(searchIndexParams) 122 | 123 | if err != nil { 124 | return nil, err 125 | } 126 | 127 | if len(resp.Payload) == 0 { 128 | return nil, fmt.Errorf("no matching entries found") 129 | } 130 | 131 | for _, v := range resp.GetPayload() { 132 | UUIDs[v] = struct{}{} 133 | } 134 | 135 | keys := make([]string, 0, len(UUIDs)) 136 | for k := range UUIDs { 137 | keys = append(keys, k) 138 | } 139 | 140 | return keys, nil 141 | } 142 | 143 | func getEntry(rekorClient *client.Rekor, UUID string) (*models.LogEntryAnon, error) { 144 | getLogEntryByUUIDParams := entries.NewGetLogEntryByUUIDParams() 145 | getLogEntryByUUIDParams.SetTimeout(time.Minute) 146 | getLogEntryByUUIDParams.EntryUUID = UUID 147 | response, err := rekorClient.Entries.GetLogEntryByUUID(getLogEntryByUUIDParams) 148 | if err != nil { 149 | return nil, err 150 | } 151 | 152 | // NOTE: Because of code gen, the response payload is just a list of length 1 153 | // except in cases of error so we can just return the first thing. 154 | for _, entry := range response.Payload { 155 | return &entry, nil 156 | } 157 | return nil, fmt.Errorf("Payload was empty") 158 | } 159 | 160 | func parseEntry(entry *models.LogEntryAnon) (*RekorRecord, error) { 161 | b, err := base64.StdEncoding.DecodeString(entry.Body.(string)) 162 | if err != nil { 163 | return nil, err 164 | } 165 | 166 | pe, err := models.UnmarshalProposedEntry(bytes.NewReader(b), runtime.JSONConsumer()) 167 | if err != nil { 168 | return nil, err 169 | } 170 | 171 | switch pe.Kind() { 172 | case "intoto": 173 | bodyJson, err := pe.(*models.Intoto).MarshalJSON() 174 | var intoto models.Intoto 175 | err = json.Unmarshal(bodyJson, &intoto) 176 | if err != nil { 177 | return nil, err 178 | } 179 | 180 | statement, err := parseStatement(string(entry.Attestation.Data)) 181 | if err != nil { 182 | return nil, err 183 | } 184 | 185 | return &RekorRecord{ 186 | Body: string(bodyJson), 187 | Attestation: statement, 188 | }, nil 189 | 190 | default: 191 | return nil, fmt.Errorf("%s is an unsupported entry type", pe.Kind()) 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /testdata/bar.json: -------------------------------------------------------------------------------- 1 | { 2 | "_type": "https://in-toto.io/Statement/v0.1", 3 | "subject": [ 4 | { 5 | "name": "bar", 6 | "digest": { 7 | "sha256": "aa3ef4cc044edf83a4c0e2dbce4c5a71ff98bc9f3598a1b359471db93bb464e3" 8 | } 9 | } 10 | ], 11 | "predicateType": "https://slsa.dev/provenance/v0.2", 12 | "predicate": { 13 | "builder": { 14 | "id": "test://builder" 15 | }, 16 | "buildType": "test://none", 17 | "materials": [ 18 | { 19 | "uri": "test://baz", 20 | "digest": { 21 | "sha256": "40ba4fd42d1f3810027d32af727c8b6a7af55d7b345372da8d7bb63cbacada2e" 22 | } 23 | } 24 | ] 25 | } 26 | } -------------------------------------------------------------------------------- /testdata/baz.json: -------------------------------------------------------------------------------- 1 | { 2 | "_type": "https://in-toto.io/Statement/v0.1", 3 | "subject": [ 4 | { 5 | "name": "baz", 6 | "digest": { 7 | "sha256": "40ba4fd42d1f3810027d32af727c8b6a7af55d7b345372da8d7bb63cbacada2e" 8 | } 9 | } 10 | ], 11 | "predicateType": "https://slsa.dev/provenance/v0.2", 12 | "predicate": { 13 | "builder": { 14 | "id": "test://builder" 15 | }, 16 | "buildType": "test://none", 17 | "materials": [ 18 | { 19 | "uri": "test://qux", 20 | "digest": { 21 | "sha256": "f2374c128e57b739d3afc8a5627e3546bf8a666a7196f072fb9bd4caebba961a" 22 | } 23 | } 24 | ] 25 | } 26 | } -------------------------------------------------------------------------------- /testdata/foo.json: -------------------------------------------------------------------------------- 1 | { 2 | "_type": "https://in-toto.io/Statement/v0.1", 3 | "subject": [ 4 | { 5 | "name": "foo", 6 | "digest": { 7 | "sha256": "e4b8e16b932b86afa001d96296b2baa3a11c0e5ae732b95ebe2fe88d32e9f484" 8 | } 9 | } 10 | ], 11 | "predicateType": "https://slsa.dev/provenance/v0.2", 12 | "predicate": { 13 | "builder": { 14 | "id": "test://builder" 15 | }, 16 | "buildType": "test://none", 17 | "materials": [ 18 | { 19 | "uri": "test://bar", 20 | "digest": { 21 | "sha256": "aa3ef4cc044edf83a4c0e2dbce4c5a71ff98bc9f3598a1b359471db93bb464e3" 22 | } 23 | }, 24 | { 25 | "uri": "test://waldo", 26 | "digest": { 27 | "sha256": "9462b403383a6b63283b50958ebdd786f2c2dc63ac088fe8125e37d772fb4123" 28 | } 29 | } 30 | ] 31 | } 32 | } -------------------------------------------------------------------------------- /testdata/qux.json: -------------------------------------------------------------------------------- 1 | { 2 | "_type": "https://in-toto.io/Statement/v0.1", 3 | "subject": [ 4 | { 5 | "name": "qux", 6 | "digest": { 7 | "sha256": "f2374c128e57b739d3afc8a5627e3546bf8a666a7196f072fb9bd4caebba961a" 8 | } 9 | } 10 | ], 11 | "predicateType": "https://slsa.dev/provenance/v0.2", 12 | "predicate": { 13 | "builder": { 14 | "id": "test://builder" 15 | }, 16 | "buildType": "test://none" 17 | } 18 | } -------------------------------------------------------------------------------- /testdata/waldo.json: -------------------------------------------------------------------------------- 1 | { 2 | "_type": "https://in-toto.io/Statement/v0.1", 3 | "subject": [ 4 | { 5 | "name": "waldo", 6 | "digest": { 7 | "sha256": "9462b403383a6b63283b50958ebdd786f2c2dc63ac088fe8125e37d772fb4123" 8 | } 9 | } 10 | ], 11 | "predicateType": "https://slsa.dev/provenance/v0.2", 12 | "predicate": { 13 | "builder": { 14 | "id": "test://builder" 15 | }, 16 | "buildType": "test://none" 17 | } 18 | } --------------------------------------------------------------------------------