├── .gitignore ├── .goreleaser.yml ├── LICENSE.txt ├── README.md ├── config ├── config.go ├── config_test.go ├── parse.go ├── parse_test.go └── testdata │ ├── basic.hcl │ ├── basic.hcl.golden │ ├── basic.json │ └── basic.json.golden ├── go.mod ├── go.sum ├── license ├── finder.go ├── github │ ├── detect.go │ └── repo_api.go ├── golang │ ├── translator.go │ └── translator_test.go ├── gopkg │ ├── translate.go │ └── translate_test.go ├── license.go ├── mapper │ ├── finder.go │ ├── translate.go │ └── translate_test.go ├── mock_Finder.go ├── mock_StatusListener.go ├── resolver │ ├── translate.go │ └── translate_test.go ├── status.go └── status_test.go ├── main.go ├── module ├── module.go ├── module_test.go ├── sort.go └── sort_test.go ├── output.go ├── output_multi.go ├── output_terminal.go ├── output_xlsx.go └── semaphore.go /.gitignore: -------------------------------------------------------------------------------- 1 | golicense 2 | config.hcl 3 | dist/ 4 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | before: 2 | hooks: 3 | - go mod download 4 | 5 | builds: 6 | - env: 7 | - CGO_ENABLED=0 8 | goos: 9 | - darwin 10 | - linux 11 | - freebsd 12 | - windows 13 | 14 | archive: 15 | replacements: 16 | darwin: macos 17 | 386: i386 18 | amd64: x86_64 19 | 20 | checksum: 21 | name_template: 'checksums.txt' 22 | 23 | snapshot: 24 | name_template: "{{ .Tag }}-next" 25 | 26 | changelog: 27 | sort: asc 28 | filters: 29 | exclude: 30 | - 'README' 31 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Mitchell Hashimoto 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # golicense - Go Binary OSS License Scanner 2 | 3 | golicense is a tool that scans [compiled Go binaries](https://golang.org/) 4 | and can output all the dependencies, their versions, and their respective 5 | licenses (if known). golicense only works with Go binaries compiled using 6 | Go modules for dependency management. 7 | 8 | golicense determines the dependency list quickly and with exact accuracy 9 | since it uses metadata from the Go compiler to determine the _exact_ set of 10 | dependencies embedded in a compiled Go binary. This excludes dependencies that 11 | are not used in the final binary. For example, if a library depends on "foo" 12 | in function "F" but "F" is never called, then the dependency "foo" will not 13 | be present in the final binary. 14 | 15 | golicense is not meant to be a complete replacement for open source compliance 16 | companies such as [FOSSA](https://fossa.io/) or 17 | [BlackDuck](https://www.blackducksoftware.com/black-duck-home), both of 18 | which provide hundreds of additional features related to open source 19 | compliance. 20 | 21 | **Warning:** The binary itself must be trusted and untampered with to provide 22 | accurate results. It is trivial to modify the dependency information of a 23 | compiled binary. This is the opposite side of the same coin with source-based 24 | dependency analysis where the source must not be tampered. 25 | 26 | ## Features 27 | 28 | * List dependencies and their associated licenses 29 | * Cross-reference dependency licenses against an allow/deny list 30 | * Output reports in the terminal and Excel (XLSX) format 31 | * Manually specify overrides for specific dependencies if the detection 32 | is incorrect. 33 | 34 | ## Example 35 | 36 | The example below runs `golicense` against itself from a recent build. 37 | 38 | ![golicense Example](https://user-images.githubusercontent.com/1299/48667166-468d1080-ea85-11e8-8005-5a44c6a0d10a.gif) 39 | 40 | ## Installation 41 | 42 | To install `golicense`, download the appropriate release for your platform 43 | from the [releases page](https://github.com/mitchellh/golicense/releases). 44 | 45 | You can also compile from source using Go 1.11 or later using standard 46 | `go build`. Please ensure that Go modules are enabled (GOPATH not set or 47 | `GO111MODULE` set to "on"). 48 | 49 | ## Usage 50 | 51 | `golicense` is used with one or two required arguments. In the one-argument 52 | form, the dependencies and their licenses are listed. In the two-argument 53 | form, a configuration file can be given to specify an allow/deny list of 54 | licenses and more. 55 | 56 | ``` 57 | $ golicense [flags] [BINARY] 58 | $ golicense [flags] [CONFIG] [BINARY] 59 | ``` 60 | 61 | You may also pass mutliple binaries (but only if you are providing a CONFIG). 62 | 63 | ### Configuration File 64 | 65 | The configuration file can specify allow/deny lists of licenses for reports, 66 | license overrides for specific dependencies, and more. The configuration file 67 | format is [HCL](https://github.com/hashicorp/hcl2) or JSON. 68 | 69 | Example: 70 | 71 | ```hcl 72 | allow = ["MIT", "Apache-2.0"] 73 | deny = ["GNU General Public License v2.0"] 74 | ``` 75 | 76 | ```json 77 | { 78 | "allow": ["MIT", "Apache-2.0"], 79 | "deny": ["GNU General Public License v2.0"] 80 | } 81 | ``` 82 | 83 | Supported configurations: 84 | 85 | * `allow` (`array`) - A list of names or SPDX IDs of allowed licenses. 86 | * `deny` (`array`) - A list of names or SPDX IDs of denied licenses. 87 | * `override` (`map`) - A mapping of Go import identifiers 88 | to translate into a specific license by SPDX ID. This can be used to 89 | set the license of imports that `golicense` cannot detect so that reports 90 | pass. 91 | * `translate` (`map`) - A mapping of Go import identifiers 92 | to translate into alternate import identifiers. Example: 93 | "gopkg.in/foo/bar.v2" to "github.com/foo/bar". If the map key starts and 94 | ends with `/` then it is treated as a regular expression. In this case, 95 | the map value can use `\1`, `\2`, etc. to reference capture groups. 96 | 97 | ### GitHub Authentication 98 | 99 | `golicense` uses the GitHub API to look up licenses. This doesn't require 100 | any authentication out of the box but will be severely rate limited. 101 | It is recommended that you generate a [personal access token](https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line/) to increase the rate limit. The personal access token doesn't require any 102 | special access unless it needs to look at private repositories you have 103 | access to, in which case it should be granted the `repo` permission. 104 | Specify your token using the `GITHUB_TOKEN` environment variable. 105 | 106 | ``` 107 | $ export GITHUB_TOKEN=abcd1234 108 | $ golicense ./binary 109 | ``` 110 | 111 | ### Excel (XLSX) Reporting Output 112 | 113 | If the `-out-xlsx` flag is specified, then an Excel report is generated 114 | and written to the path specified in addition to the terminal output. 115 | 116 | ``` 117 | $ golicense -out-xlsx=report.xlsx ./my-program 118 | ``` 119 | 120 | The Excel report contains the list of dependencies, their versions, the 121 | detected license, and whether the license is allowed or not. The dependencies 122 | are listed in alphabetical order. The row of the dependency will have a 123 | green background if everything is okay, a yellow background if a 124 | license is unknown, or a red background is a license is denied. An example 125 | screenshot is shown below: 126 | 127 | ![Excel Report](https://user-images.githubusercontent.com/1299/48667086-84893500-ea83-11e8-925c-7929ed441b1b.png) 128 | 129 | ## Limitations 130 | 131 | There are a number of limitations to `golicense` currently. These are fixable 132 | but work hasn't been done to address these yet. If you feel like taking a stab 133 | at any of these, please do and contribute! 134 | 135 | **GitHub API:** The license detected by `golicense` may be incorrect if 136 | a GitHub project changes licenses. `golicense` uses the GitHub API which only 137 | returns the license currently detected; we can't lookup licenses for specific 138 | commit hashes. 139 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/mitchellh/golicense/license" 7 | ) 8 | 9 | // Config is the configuration structure for the license checker. 10 | type Config struct { 11 | // Allow and Deny are the list of licenses that are allowed or disallowed, 12 | // respectively. The string value here can be either the license name 13 | // (case insensitive) or the SPDX ID (case insensitive). 14 | // 15 | // If a license is found that isn't in either list, then a warning is 16 | // emitted. If a license is in both deny and allow, then deny takes 17 | // priority. 18 | Allow []string `hcl:"allow,optional"` 19 | Deny []string `hcl:"deny,optional"` 20 | 21 | // Override is a map that explicitly sets the license for the given 22 | // import path. The key is an import path (exact) and the value is 23 | // the name or SPDX ID of the license. Regardless, the value will 24 | // be set as both the name and SPDX ID, so SPDX IDs are recommended. 25 | Override map[string]string `hcl:"override,optional"` 26 | 27 | // Translate is a map that translates one import source into another. 28 | // For example, "gopkg.in/(.*)" => "github.com/\1" would translate 29 | // gopkg into github (incorrectly, but the example would work). 30 | Translate map[string]string `hcl:"translate,optional"` 31 | } 32 | 33 | // Allowed returns the allowed state of a license given the configuration. 34 | func (c *Config) Allowed(l *license.License) AllowState { 35 | if l == nil { 36 | return StateDenied // no license is never allowed 37 | } 38 | 39 | name := strings.ToLower(l.Name) 40 | spdx := strings.ToLower(l.SPDX) 41 | 42 | // Deny takes priority 43 | for _, v := range c.Deny { 44 | v = strings.ToLower(v) 45 | if name == v || spdx == v { 46 | return StateDenied 47 | } 48 | } 49 | 50 | for _, v := range c.Allow { 51 | v = strings.ToLower(v) 52 | if name == v || spdx == v { 53 | return StateAllowed 54 | } 55 | } 56 | 57 | return StateUnknown 58 | } 59 | 60 | type AllowState int 61 | 62 | const ( 63 | StateUnknown AllowState = iota 64 | StateAllowed 65 | StateDenied 66 | ) 67 | -------------------------------------------------------------------------------- /config/config_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/mitchellh/golicense/license" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestConfigAllowed(t *testing.T) { 11 | cases := []struct { 12 | Name string 13 | Config *Config 14 | Lic *license.License 15 | Result AllowState 16 | }{ 17 | { 18 | "empty lists", 19 | &Config{}, 20 | &license.License{Name: "FOO"}, 21 | StateUnknown, 22 | }, 23 | 24 | { 25 | "name allowed", 26 | &Config{ 27 | Allow: []string{"FOO"}, 28 | }, 29 | &license.License{Name: "FOO"}, 30 | StateAllowed, 31 | }, 32 | 33 | { 34 | "name allowed and denied", 35 | &Config{ 36 | Allow: []string{"FOO"}, 37 | Deny: []string{"FOO"}, 38 | }, 39 | &license.License{Name: "FOO"}, 40 | StateDenied, 41 | }, 42 | 43 | { 44 | "spdx allowed", 45 | &Config{ 46 | Allow: []string{"FOO"}, 47 | }, 48 | &license.License{SPDX: "FOO"}, 49 | StateAllowed, 50 | }, 51 | 52 | { 53 | "spdx allowed and denied", 54 | &Config{ 55 | Allow: []string{"FOO"}, 56 | Deny: []string{"FOO"}, 57 | }, 58 | &license.License{SPDX: "FOO"}, 59 | StateDenied, 60 | }, 61 | } 62 | 63 | for _, tt := range cases { 64 | t.Run(tt.Name, func(t *testing.T) { 65 | actual := tt.Config.Allowed(tt.Lic) 66 | require.Equal(t, actual, tt.Result) 67 | }) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /config/parse.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "io/ioutil" 7 | "os" 8 | "path/filepath" 9 | 10 | "github.com/hashicorp/hcl2/gohcl" 11 | "github.com/hashicorp/hcl2/hcl" 12 | "github.com/hashicorp/hcl2/hcl/hclsyntax" 13 | "github.com/hashicorp/hcl2/hcl/json" 14 | ) 15 | 16 | // ParseFile parses the given file for a configuration. The syntax of the 17 | // file is determined based on the filename extension: "hcl" for HCL, 18 | // "json" for JSON, other is an error. 19 | func ParseFile(filename string) (*Config, error) { 20 | f, err := os.Open(filename) 21 | if err != nil { 22 | return nil, err 23 | } 24 | defer f.Close() 25 | 26 | ext := filepath.Ext(filename) 27 | if len(ext) > 0 { 28 | ext = ext[1:] 29 | } 30 | 31 | return Parse(f, filename, ext) 32 | } 33 | 34 | // Parse parses the configuration from the given reader. The reader will be 35 | // read to completion (EOF) before returning so ensure that the reader 36 | // does not block forever. 37 | // 38 | // format is either "hcl" or "json" 39 | func Parse(r io.Reader, filename, format string) (*Config, error) { 40 | switch format { 41 | case "hcl": 42 | return parseHCL(r, filename) 43 | 44 | case "json": 45 | return parseJSON(r, filename) 46 | 47 | default: 48 | return nil, fmt.Errorf("Format must be either 'hcl' or 'json'") 49 | } 50 | } 51 | 52 | func parseHCL(r io.Reader, filename string) (*Config, error) { 53 | src, err := ioutil.ReadAll(r) 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | f, diag := hclsyntax.ParseConfig(src, filename, hcl.Pos{}) 59 | if diag.HasErrors() { 60 | return nil, diag 61 | } 62 | 63 | var config Config 64 | diag = gohcl.DecodeBody(f.Body, nil, &config) 65 | if diag.HasErrors() { 66 | return nil, diag 67 | } 68 | 69 | return &config, nil 70 | } 71 | 72 | func parseJSON(r io.Reader, filename string) (*Config, error) { 73 | src, err := ioutil.ReadAll(r) 74 | if err != nil { 75 | return nil, err 76 | } 77 | 78 | f, diag := json.Parse(src, filename) 79 | if diag.HasErrors() { 80 | return nil, diag 81 | } 82 | 83 | var config Config 84 | diag = gohcl.DecodeBody(f.Body, nil, &config) 85 | if diag.HasErrors() { 86 | return nil, diag 87 | } 88 | 89 | return &config, nil 90 | } 91 | -------------------------------------------------------------------------------- /config/parse_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | 8 | "github.com/davecgh/go-spew/spew" 9 | "github.com/sebdah/goldie" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func init() { 14 | goldie.FixtureDir = "testdata" 15 | spew.Config.DisablePointerAddresses = true 16 | } 17 | 18 | func TestParseFile(t *testing.T) { 19 | f, err := os.Open("testdata") 20 | require.NoError(t, err) 21 | defer f.Close() 22 | 23 | fis, err := f.Readdir(-1) 24 | require.NoError(t, err) 25 | for _, fi := range fis { 26 | if fi.IsDir() { 27 | continue 28 | } 29 | 30 | if filepath.Ext(fi.Name()) == ".golden" { 31 | continue 32 | } 33 | 34 | t.Run(fi.Name(), func(t *testing.T) { 35 | cfg, err := ParseFile(filepath.Join("testdata", fi.Name())) 36 | require.NoError(t, err) 37 | goldie.Assert(t, fi.Name(), []byte(spew.Sdump(cfg))) 38 | }) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /config/testdata/basic.hcl: -------------------------------------------------------------------------------- 1 | allow = ["one", 2 | "two", "three/four"] 3 | -------------------------------------------------------------------------------- /config/testdata/basic.hcl.golden: -------------------------------------------------------------------------------- 1 | (*config.Config)({ 2 | Allow: ([]string) (len=3 cap=3) { 3 | (string) (len=3) "one", 4 | (string) (len=3) "two", 5 | (string) (len=10) "three/four" 6 | }, 7 | Deny: ([]string) , 8 | Override: (map[string]string) , 9 | Translate: (map[string]string) 10 | }) 11 | -------------------------------------------------------------------------------- /config/testdata/basic.json: -------------------------------------------------------------------------------- 1 | { 2 | "allow": ["one", "two", "three/four"] 3 | } 4 | -------------------------------------------------------------------------------- /config/testdata/basic.json.golden: -------------------------------------------------------------------------------- 1 | (*config.Config)({ 2 | Allow: ([]string) (len=3 cap=3) { 3 | (string) (len=3) "one", 4 | (string) (len=3) "two", 5 | (string) (len=10) "three/four" 6 | }, 7 | Deny: ([]string) , 8 | Override: (map[string]string) , 9 | Translate: (map[string]string) 10 | }) 11 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mitchellh/golicense 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/360EntSecGroup-Skylar/excelize v1.4.0 7 | github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 // indirect 8 | github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 // indirect 9 | github.com/davecgh/go-spew v1.1.1 10 | github.com/dgryski/go-metro v0.0.0-20180109044635-280f6062b5bc // indirect 11 | github.com/dgryski/go-minhash v0.0.0-20170608043002-7fe510aff544 // indirect 12 | github.com/dgryski/go-spooky v0.0.0-20170606183049-ed3d087f40e2 // indirect 13 | github.com/ekzhu/minhash-lsh v0.0.0-20171225071031-5c06ee8586a1 // indirect 14 | github.com/emirpasic/gods v1.12.0 // indirect 15 | github.com/fatih/color v1.7.0 16 | github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect 17 | github.com/gliderlabs/ssh v0.2.2 // indirect 18 | github.com/google/go-github/v18 v18.2.0 19 | github.com/gosuri/uilive v0.0.0-20170323041506-ac356e6e42cd 20 | github.com/hashicorp/go-multierror v1.0.0 21 | github.com/hashicorp/hcl2 v0.0.0-20181111172936-0467c0c38ca2 22 | github.com/hhatto/gorst v0.0.0-20181029133204-ca9f730cac5b // indirect 23 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect 24 | github.com/jdkato/prose v1.1.0 // indirect 25 | github.com/kevinburke/ssh_config v0.0.0-20180830205328-81db2a75821e // indirect 26 | github.com/mattn/go-colorable v0.0.9 // indirect 27 | github.com/mattn/go-isatty v0.0.4 // indirect 28 | github.com/mitchellh/go-homedir v1.0.0 // indirect 29 | github.com/mitchellh/go-spdx v0.1.0 30 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect 31 | github.com/montanaflynn/stats v0.0.0-20180911141734-db72e6cae808 // indirect 32 | github.com/neurosnap/sentences v1.0.6 // indirect 33 | github.com/pelletier/go-buffruneio v0.2.0 // indirect 34 | github.com/pkg/errors v0.8.0 // indirect 35 | github.com/rsc/goversion v1.2.0 36 | github.com/sebdah/goldie v0.0.0-20180424091453-8784dd1ab561 37 | github.com/shogo82148/go-shuffle v0.0.0-20180218125048-27e6095f230d // indirect 38 | github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95 // indirect 39 | github.com/src-d/gcfg v1.4.0 // indirect 40 | github.com/stretchr/objx v0.1.1 // indirect 41 | github.com/stretchr/testify v1.2.2 42 | github.com/xanzy/ssh-agent v0.2.0 // indirect 43 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 44 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be 45 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846 46 | gonum.org/v1/netlib v0.0.0-20191031114514-eccb95939662 // indirect 47 | gopkg.in/neurosnap/sentences.v1 v1.0.6 // indirect 48 | gopkg.in/russross/blackfriday.v2 v2.0.0 // indirect 49 | gopkg.in/src-d/go-billy-siva.v4 v4.2.2 // indirect 50 | gopkg.in/src-d/go-billy.v4 v4.3.0 // indirect 51 | gopkg.in/src-d/go-git-fixtures.v3 v3.5.0 // indirect 52 | gopkg.in/src-d/go-git.v4 v4.7.0 // indirect 53 | gopkg.in/src-d/go-license-detector.v2 v2.0.0-20180510072912-da552ecf050b 54 | gopkg.in/src-d/go-siva.v1 v1.3.0 // indirect 55 | gopkg.in/warnings.v0 v0.1.2 // indirect 56 | ) 57 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/360EntSecGroup-Skylar/excelize v1.4.0 h1:43rak9uafmwSJpXfFO1heKQph8tP3nlfWJWFQQtW1R0= 2 | github.com/360EntSecGroup-Skylar/excelize v1.4.0/go.mod h1:R8KYLmGns0vDPe6/HyphW0mzW+MFexlGDafU0ykVEnU= 3 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 4 | github.com/DHowett/go-plist v0.0.0-20180609054337-500bd5b9081b/go.mod h1:5paT5ZDrOm8eAJPem2Bd+q3FTi3Gxm/U4tb2tH8YIUQ= 5 | github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8= 6 | github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= 7 | github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= 8 | github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs= 9 | github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs= 10 | github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA= 11 | github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= 12 | github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM= 13 | github.com/apparentlymart/go-textseg v1.0.0 h1:rRmlIsPEEhUTIKQb7T++Nz/A5Q6C9IuX2wFoYVvnCs0= 14 | github.com/apparentlymart/go-textseg v1.0.0/go.mod h1:z96Txxhf3xSFMPmb5X/1W05FF/Nj9VFpLOpjS5yuumk= 15 | github.com/bsm/go-vlq v0.0.0-20150828105119-ec6e8d4f5f4e/go.mod h1:N+BjUcTjSxc2mtRGSCPsat1kze3CUtvJN3/jTXlp29k= 16 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 17 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 18 | github.com/dgryski/go-metro v0.0.0-20180109044635-280f6062b5bc h1:8WFBn63wegobsYAX0YjD+8suexZDga5CctH4CCTx2+8= 19 | github.com/dgryski/go-metro v0.0.0-20180109044635-280f6062b5bc/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw= 20 | github.com/dgryski/go-minhash v0.0.0-20170608043002-7fe510aff544 h1:54Y/2GF52MSJ4n63HWvNDFRtztgm6tq2UrOX61sjGKc= 21 | github.com/dgryski/go-minhash v0.0.0-20170608043002-7fe510aff544/go.mod h1:VBi0XHpFy0xiMySf6YpVbRqrupW4RprJ5QTyN+XvGSM= 22 | github.com/dgryski/go-spooky v0.0.0-20170606183049-ed3d087f40e2 h1:lx1ZQgST/imDhmLpYDma1O3Cx9L+4Ie4E8S2RjFPQ30= 23 | github.com/dgryski/go-spooky v0.0.0-20170606183049-ed3d087f40e2/go.mod h1:hgHYKsoIw7S/hlWtP7wD1wZ7SX1jPTtKko5X9jrOgPQ= 24 | github.com/ekzhu/minhash-lsh v0.0.0-20171225071031-5c06ee8586a1 h1:/7G7q8SDJdrah5jDYqZI8pGFjSqiCzfSEO+NgqKCYX0= 25 | github.com/ekzhu/minhash-lsh v0.0.0-20171225071031-5c06ee8586a1/go.mod h1:yEtCVi+QamvzjEH4U/m6ZGkALIkF2xfQnFp0BcKmIOk= 26 | github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg= 27 | github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= 28 | github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= 29 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 30 | github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ= 31 | github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= 32 | github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= 33 | github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0= 34 | github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= 35 | github.com/go-test/deep v1.0.1 h1:UQhStjbkDClarlmv0am7OXXO4/GaPdCGiUiMTvi28sg= 36 | github.com/go-test/deep v1.0.1/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= 37 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= 38 | github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 39 | github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= 40 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 41 | github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= 42 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 43 | github.com/google/go-github/v18 v18.2.0 h1:s7Y4I8ZlIL1Ofxpj4AQRJcSGAr0Jl2AHThHgXpsUCfU= 44 | github.com/google/go-github/v18 v18.2.0/go.mod h1:Bf4Ut1RTeH0WuX7Z4Zf7N+qp/YqgcFOxvTLuSO+aY/k= 45 | github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= 46 | github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= 47 | github.com/gosuri/uilive v0.0.0-20170323041506-ac356e6e42cd h1:1e+0Z+T4t1mKL5xxvxXh5FkjuiToQGKreCobLu7lR3Y= 48 | github.com/gosuri/uilive v0.0.0-20170323041506-ac356e6e42cd/go.mod h1:qkLSc0A5EXSP6B04TrN4oQoxqFI7A8XvoXSlJi8cwk8= 49 | github.com/hashicorp/errwrap v0.0.0-20180715044906-d6c0cd880357/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 50 | github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= 51 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 52 | github.com/hashicorp/go-cleanhttp v0.5.0 h1:wvCrVc9TjDls6+YGAF2hAifE1E5U1+b4tH6KdvN3Gig= 53 | github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= 54 | github.com/hashicorp/go-multierror v0.0.0-20180717150148-3d5d8f294aa0/go.mod h1:JMRHfdO9jKNzS/+BTlxCjKNQHg/jZAft8U7LloJvN7I= 55 | github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o= 56 | github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= 57 | github.com/hashicorp/hcl2 v0.0.0-20181111172936-0467c0c38ca2 h1:uNBHbPmzVQJHlOPwdj/2Ssi9WMXBYaPfaq5pylaJJuQ= 58 | github.com/hashicorp/hcl2 v0.0.0-20181111172936-0467c0c38ca2/go.mod h1:4nBvwJRETsbpa0LQ7FbXXVFmo0Crvhya1Dmpbm7cVow= 59 | github.com/hhatto/gorst v0.0.0-20181029133204-ca9f730cac5b h1:Jdu2tbAxkRouSILp2EbposIb8h4gO+2QuZEn3d9sKAc= 60 | github.com/hhatto/gorst v0.0.0-20181029133204-ca9f730cac5b/go.mod h1:HmaZGXHdSwQh1jnUlBGN2BeEYOHACLVGzYOXCbsLvxY= 61 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= 62 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= 63 | github.com/jdkato/prose v1.1.0 h1:LpvmDGwbKGTgdCH3a8VJL56sr7p/wOFPw/R4lM4PfFg= 64 | github.com/jdkato/prose v1.1.0/go.mod h1:jkF0lkxaX5PFSlk9l4Gh9Y+T57TqUZziWT7uZbW5ADg= 65 | github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= 66 | github.com/kevinburke/ssh_config v0.0.0-20180830205328-81db2a75821e h1:RgQk53JHp/Cjunrr1WlsXSZpqXn+uREuHvUVcK82CV8= 67 | github.com/kevinburke/ssh_config v0.0.0-20180830205328-81db2a75821e/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= 68 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 69 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 70 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 71 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 72 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 73 | github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4= 74 | github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= 75 | github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= 76 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 77 | github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= 78 | github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 79 | github.com/mitchellh/go-homedir v1.0.0 h1:vKb8ShqSby24Yrqr/yDYkuFz8d0WUjys40rvnGC8aR0= 80 | github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 81 | github.com/mitchellh/go-spdx v0.1.0 h1:50JnVzkL3kWreQ5Qb4Pi3Qx9e+bbYrt8QglJDpfeBEs= 82 | github.com/mitchellh/go-spdx v0.1.0/go.mod h1:FFi4Cg1fBuN/JCtPtP8PEDmcBjvO3gijQVl28YjIBVQ= 83 | github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM= 84 | github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= 85 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= 86 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= 87 | github.com/montanaflynn/stats v0.0.0-20180911141734-db72e6cae808 h1:pmpDGKLw4n82EtrNiLqB+xSz/JQwFOaZuMALYUHwX5s= 88 | github.com/montanaflynn/stats v0.0.0-20180911141734-db72e6cae808/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= 89 | github.com/neurosnap/sentences v1.0.6 h1:iBVUivNtlwGkYsJblWV8GGVFmXzZzak907Ci8aA0VTE= 90 | github.com/neurosnap/sentences v1.0.6/go.mod h1:pg1IapvYpWCJJm/Etxeh0+gtMf1rI1STY9S7eUCPbDc= 91 | github.com/pelletier/go-buffruneio v0.2.0 h1:U4t4R6YkofJ5xHm3dJzuRpPZ0mr5MMCoAWooScCR7aA= 92 | github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo= 93 | github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= 94 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 95 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 96 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 97 | github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= 98 | github.com/rsc/goversion v1.2.0 h1:zVF4y5ciA/rw779S62bEAq4Yif1cBc/UwRkXJ2xZyT4= 99 | github.com/rsc/goversion v1.2.0/go.mod h1:Tf/O0TQyfRvp7NelXAyfXYRKUO+LX3KNgXc8ALRUv4k= 100 | github.com/sebdah/goldie v0.0.0-20180424091453-8784dd1ab561 h1:IY+sDBJR/wRtsxq+626xJnt4Tw7/ROA9cDIR8MMhWyg= 101 | github.com/sebdah/goldie v0.0.0-20180424091453-8784dd1ab561/go.mod h1:lvjGftC8oe7XPtyrOidaMi0rp5B9+XY/ZRUynGnuaxQ= 102 | github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= 103 | github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= 104 | github.com/shogo82148/go-shuffle v0.0.0-20180218125048-27e6095f230d h1:rUbV6LJa5RXK3jT/4jnJUz3UkrXzW6cqB+n9Fkbv9jY= 105 | github.com/shogo82148/go-shuffle v0.0.0-20180218125048-27e6095f230d/go.mod h1:2htx6lmL0NGLHlO8ZCf+lQBGBHIbEujyywxJArf+2Yc= 106 | github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95 h1:/vdW8Cb7EXrkqWGufVMES1OH2sU9gKVb2n9/1y5NMBY= 107 | github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 108 | github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 109 | github.com/src-d/gcfg v1.4.0 h1:xXbNR5AlLSA315x2UO+fTSSAXCDf+Ar38/6oyGbDKQ4= 110 | github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI= 111 | github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= 112 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 113 | github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= 114 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 115 | github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= 116 | github.com/xanzy/ssh-agent v0.2.0 h1:Adglfbi5p9Z0BmK2oKU9nTG+zKfniSfnaMYB+ULd+Ro= 117 | github.com/xanzy/ssh-agent v0.2.0/go.mod h1:0NyE30eGUDliuLEHJgYte/zncp2zdTStcOnWhgSqHD8= 118 | github.com/zclconf/go-cty v0.0.0-20180815031001-58bb2bc0302a h1:x70ZZ4caA8eY4abjpcCnf6uvIPY3cpgRFrXE47JF4Sc= 119 | github.com/zclconf/go-cty v0.0.0-20180815031001-58bb2bc0302a/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLEih+O3s= 120 | golang.org/x/crypto v0.0.0-20180816225734-aabede6cba87/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 121 | golang.org/x/crypto v0.0.0-20180820150726-614d502a4dac h1:7d7lG9fHOLdL6jZPtnV4LpI41SbohIJ1Atq7U991dMg= 122 | golang.org/x/crypto v0.0.0-20180820150726-614d502a4dac/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 123 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= 124 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 125 | golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 126 | golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 127 | golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 128 | golang.org/x/exp v0.0.0-20190312203227-4b39c73a6495 h1:I6A9Ag9FpEKOjcKrRNjQkPHawoXIhKyTGfvvjFAiiAk= 129 | golang.org/x/exp v0.0.0-20190312203227-4b39c73a6495/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 130 | golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= 131 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 132 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 133 | golang.org/x/net v0.0.0-20180811021610-c39426892332/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 134 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d h1:g9qWBGx4puODJTMVyoPrpoxPFgVGd+z1DZwjfRu4d0I= 135 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 136 | golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628= 137 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 138 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs= 139 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 140 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA= 141 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 142 | golang.org/x/sys v0.0.0-20180816055513-1c9583448a9c/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 143 | golang.org/x/sys v0.0.0-20180824143301-4910a1d54f87 h1:GqwDwfvIpC33dK9bA1fD+JiDUNsuAiQiEkpHqUKze4o= 144 | golang.org/x/sys v0.0.0-20180824143301-4910a1d54f87/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 145 | golang.org/x/sys v0.0.0-20180903190138-2b024373dcd9 h1:lkiLiLBHGoH3XnqSLUIaBsilGMUjI+Uy2Xu2JLUtTas= 146 | golang.org/x/sys v0.0.0-20180903190138-2b024373dcd9/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 147 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 148 | golang.org/x/sys v0.0.0-20190312061237-fead79001313 h1:pczuHS43Cp2ktBEEmLwScxgjWsBSzdaQiKzUyf3DTTc= 149 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 150 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= 151 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 152 | golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b h1:7tibmaEqrQYA+q6ri7NQjuxqSwechjtDHKq6/e85S38= 153 | golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 154 | golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 155 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846 h1:0oJP+9s5Z3MT6dym56c4f7nVeujVpL1QyD2Vp/bTql0= 156 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 157 | gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= 158 | gonum.org/v1/gonum v0.6.0 h1:DJy6UzXbahnGUf1ujUNkh/NEtK14qMo2nvlBPs4U5yw= 159 | gonum.org/v1/gonum v0.6.0/go.mod h1:9mxDZsDKxgMAuccQkewq682L+0eCu4dCN2yonUJTCLU= 160 | gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= 161 | gonum.org/v1/netlib v0.0.0-20191031114514-eccb95939662 h1:yBPy8lLj+GituDSGQjvXBqT6yTch2BdT9Z/FbX19+to= 162 | gonum.org/v1/netlib v0.0.0-20191031114514-eccb95939662/go.mod h1:1LGLsuRLSwj1ge7tgC9ees7gfh1phRP5tuyDqlpChGE= 163 | gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= 164 | google.golang.org/appengine v1.1.0 h1:igQkv0AAhEIvTEpD5LIpAfav2eeVO9HBTjvKHVJPRSs= 165 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 166 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 167 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 168 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 169 | gopkg.in/neurosnap/sentences.v1 v1.0.6 h1:v7ElyP020iEZQONyLld3fHILHWOPs+ntzuQTNPkul8E= 170 | gopkg.in/neurosnap/sentences.v1 v1.0.6/go.mod h1:YlK+SN+fLQZj+kY3r8DkGDhDr91+S3JmTb5LSxFRQo0= 171 | gopkg.in/russross/blackfriday.v2 v2.0.0 h1:+FlnIV8DSQnT7NZ43hcVKcdJdzZoeCmJj4Ql8gq5keA= 172 | gopkg.in/russross/blackfriday.v2 v2.0.0/go.mod h1:6sSBNz/GtOm/pJTuh5UmBK2ZHfmnxGbl2NZg1UliSOI= 173 | gopkg.in/src-d/go-billy-siva.v4 v4.2.2 h1:HvklDMblrg/Zknu4tAFJayg34QUOvuojXjMQGqA2FtM= 174 | gopkg.in/src-d/go-billy-siva.v4 v4.2.2/go.mod h1:4wKeCzOCSsdyFeM5+58M6ObU6FM+lZT12p7zm7A+9n0= 175 | gopkg.in/src-d/go-billy.v4 v4.3.0 h1:KtlZ4c1OWbIs4jCv5ZXrTqG8EQocr0g/d4DjNg70aek= 176 | gopkg.in/src-d/go-billy.v4 v4.3.0/go.mod h1:tm33zBoOwxjYHZIE+OV8bxTWFMJLrconzFMd38aARFk= 177 | gopkg.in/src-d/go-git-fixtures.v3 v3.5.0 h1:ivZFOIltbce2Mo8IjzUHAFoq/IylO9WHhNOAJK+LsJg= 178 | gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g= 179 | gopkg.in/src-d/go-git.v4 v4.7.0 h1:WXB+2gCoRhQiAr//IMHpIpoDsTrDgvjDORxt57e8XTA= 180 | gopkg.in/src-d/go-git.v4 v4.7.0/go.mod h1:CzbUWqMn4pvmvndg3gnh5iZFmSsbhyhUWdI0IQ60AQo= 181 | gopkg.in/src-d/go-license-detector.v2 v2.0.0-20180510072912-da552ecf050b h1:rmf1qjKdqRRm2SAc7eCLCklq+2BM4spdlykM3CVmVUc= 182 | gopkg.in/src-d/go-license-detector.v2 v2.0.0-20180510072912-da552ecf050b/go.mod h1:zfdY69eZLzMJeDFDZAVS0ZEZ98XX+SwLifFOuuUZrC0= 183 | gopkg.in/src-d/go-siva.v1 v1.3.0 h1:ARW6NvQNYIRLr0Hrftiyb7vWW9p0gWk9KchMz2hiH0I= 184 | gopkg.in/src-d/go-siva.v1 v1.3.0/go.mod h1:tk1jnIXawd/PTlRNWdr5V5lC0PttNJmu1fv7wt7IZlw= 185 | gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= 186 | gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= 187 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 188 | howett.net/plist v0.0.0-20180609054337-500bd5b9081b/go.mod h1:jInWmjR7JRkkon4jlLXDZGVEeY/wo3kOOJEWYhNE+9Y= 189 | modernc.org/cc v1.0.0/go.mod h1:1Sk4//wdnYJiUIxnW8ddKpaOJCF37yAdqYnkxUpaYxw= 190 | modernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk= 191 | modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k= 192 | modernc.org/strutil v1.1.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs= 193 | modernc.org/xc v1.0.0/go.mod h1:mRNCo0bvLjGhHO9WsyuKVU4q0ceiDDDoEeWDJHrNx8I= 194 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= 195 | -------------------------------------------------------------------------------- /license/finder.go: -------------------------------------------------------------------------------- 1 | package license 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/hashicorp/go-multierror" 7 | "github.com/mitchellh/golicense/module" 8 | ) 9 | 10 | // Finder implementations can find a license for a given module. 11 | type Finder interface { 12 | // License looks up the license for a given module. 13 | License(context.Context, module.Module) (*License, error) 14 | } 15 | 16 | // Translator implementations can convert one module path to another 17 | // module path that is more suitable for license lookup. 18 | type Translator interface { 19 | // Translate takes a module and converts it into another module. 20 | // This is used to, for example, detect gopkg.in URLs as GitHub 21 | // repositories. 22 | Translate(context.Context, module.Module) (module.Module, bool) 23 | } 24 | 25 | // Translate translates the given module or returns the same module if 26 | // no translation is necessary. 27 | func Translate(ctx context.Context, m module.Module, ts []Translator) module.Module { 28 | for _, t := range ts { 29 | n, ok := t.Translate(ctx, m) 30 | if ok { 31 | m = n 32 | } 33 | } 34 | 35 | return m 36 | } 37 | 38 | // Find finds the license for the given module using a set of finders. 39 | // 40 | // The finders are tried in the order given. The first finder to return 41 | // a non-nil License without an error is returned. If a finder returns 42 | // an error, other finders are still attempted. It is possible for a non-nil 43 | // license to be returned WITH a non-nil error meaning a different lookup 44 | // failed. 45 | func Find(ctx context.Context, m module.Module, fs []Finder) (r *License, rerr error) { 46 | for _, f := range fs { 47 | lic, err := f.License(ctx, m) 48 | if err != nil { 49 | rerr = multierror.Append(rerr, err) 50 | continue 51 | } 52 | if lic != nil { 53 | r = lic 54 | break 55 | } 56 | } 57 | 58 | return 59 | } 60 | -------------------------------------------------------------------------------- /license/github/detect.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "encoding/base64" 5 | "fmt" 6 | 7 | "github.com/google/go-github/v18/github" 8 | "github.com/mitchellh/go-spdx" 9 | "github.com/mitchellh/golicense/license" 10 | "gopkg.in/src-d/go-license-detector.v2/licensedb" 11 | "gopkg.in/src-d/go-license-detector.v2/licensedb/filer" 12 | ) 13 | 14 | // detect uses go-license-detector as a fallback. 15 | func detect(rl *github.RepositoryLicense) (*license.License, error) { 16 | ms, err := licensedb.Detect(&filerImpl{License: rl}) 17 | if err != nil { 18 | return nil, err 19 | } 20 | 21 | // Find the highest matching license 22 | var highest float32 23 | current := "" 24 | for id, v := range ms { 25 | if v > 0.90 && v > highest { 26 | highest = v 27 | current = id 28 | } 29 | } 30 | 31 | if current == "" { 32 | return nil, nil 33 | } 34 | 35 | // License detection only returns SPDX IDs but we want the complete name. 36 | lic, err := spdx.License(current) 37 | if err != nil { 38 | return nil, fmt.Errorf("error looking up license %q: %s", current, err) 39 | } 40 | 41 | return &license.License{ 42 | Name: lic.Name, 43 | SPDX: lic.ID, 44 | }, nil 45 | } 46 | 47 | // filerImpl implements filer.Filer to return the license text directly 48 | // from the github.RepositoryLicense structure. 49 | type filerImpl struct { 50 | License *github.RepositoryLicense 51 | } 52 | 53 | func (f *filerImpl) ReadFile(name string) ([]byte, error) { 54 | if name != "LICENSE" { 55 | return nil, fmt.Errorf("unknown file: %s", name) 56 | } 57 | 58 | return base64.StdEncoding.DecodeString(f.License.GetContent()) 59 | } 60 | 61 | func (f *filerImpl) ReadDir(dir string) ([]filer.File, error) { 62 | // We only support root 63 | if dir != "" { 64 | return nil, nil 65 | } 66 | 67 | return []filer.File{filer.File{Name: "LICENSE"}}, nil 68 | } 69 | 70 | func (f *filerImpl) Close() {} 71 | -------------------------------------------------------------------------------- /license/github/repo_api.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "regexp" 7 | "time" 8 | 9 | "github.com/google/go-github/v18/github" 10 | "github.com/mitchellh/golicense/license" 11 | "github.com/mitchellh/golicense/module" 12 | ) 13 | 14 | // RepoAPI implements license.Finder and looks up the license of a module 15 | // using the GitHub Repository License API[1]. 16 | // 17 | // This API will return the detected license based on the current source code. 18 | // Therefore it is theoretically possible for a dependency to have a different 19 | // license based on the exact match of the SHA (the project changed licenses). 20 | // In practice, this is quite rare so it is up to the caller to determine if 21 | // this is an acceptable risk or not. 22 | // 23 | // [1]: https://developer.github.com/v3/licenses/#get-the-contents-of-a-repositorys-license 24 | type RepoAPI struct { 25 | Client *github.Client 26 | } 27 | 28 | // License implements license.Finder 29 | func (f *RepoAPI) License(ctx context.Context, m module.Module) (*license.License, error) { 30 | matches := githubRe.FindStringSubmatch(m.Path) 31 | if matches == nil { 32 | return nil, nil 33 | } 34 | 35 | FETCH_RETRY: 36 | license.UpdateStatus(ctx, license.StatusNormal, "querying license") 37 | rl, _, err := f.Client.Repositories.License(ctx, matches[1], matches[2]) 38 | if rateErr, ok := err.(*github.RateLimitError); ok { 39 | dur := time.Until(rateErr.Rate.Reset.Time) 40 | timer := time.NewTimer(dur) 41 | defer timer.Stop() 42 | license.UpdateStatus(ctx, license.StatusWarning, fmt.Sprintf( 43 | "rate limited by GitHub, waiting %s", dur)) 44 | 45 | select { 46 | case <-ctx.Done(): 47 | // Context cancelled or ended so return early 48 | return nil, ctx.Err() 49 | 50 | case <-timer.C: 51 | // Rate limit should be up, retry 52 | goto FETCH_RETRY 53 | } 54 | } 55 | if err != nil { 56 | return nil, err 57 | } 58 | 59 | // If the license type is "other" then we try to use go-license-detector 60 | // to determine the license, which seems to be accurate in these cases. 61 | if rl.GetLicense().GetKey() == "other" { 62 | return detect(rl) 63 | } 64 | 65 | return &license.License{ 66 | Name: rl.GetLicense().GetName(), 67 | SPDX: rl.GetLicense().GetSPDXID(), 68 | }, nil 69 | } 70 | 71 | // githubRe is the regexp matching the package for a GitHub import. 72 | var githubRe = regexp.MustCompile(`^github\.com/([^/]+)/([^/]+)$`) 73 | -------------------------------------------------------------------------------- /license/golang/translator.go: -------------------------------------------------------------------------------- 1 | package golang 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "regexp" 7 | 8 | "github.com/mitchellh/golicense/module" 9 | ) 10 | 11 | type Translator struct{} 12 | 13 | func (t Translator) Translate(ctx context.Context, m module.Module) (module.Module, bool) { 14 | ms := re.FindStringSubmatch(m.Path) 15 | if ms == nil { 16 | return module.Module{}, false 17 | } 18 | 19 | // Matches, convert to github 20 | m.Path = fmt.Sprintf("github.com/golang/%s", ms[1]) 21 | return m, true 22 | } 23 | 24 | // re is the regexp matching the package for a GoPkg import. This is taken 25 | // almost directly from the GoPkg source code itself so it should match 26 | // perfectly. 27 | var re = regexp.MustCompile(`^go\.googlesource\.com/([^/]+)$`) 28 | -------------------------------------------------------------------------------- /license/golang/translator_test.go: -------------------------------------------------------------------------------- 1 | package golang 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/mitchellh/golicense/module" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestTranslator(t *testing.T) { 12 | cases := []struct { 13 | Input string 14 | Output string 15 | }{ 16 | { 17 | "github.com/foo/bar", 18 | "", 19 | }, 20 | 21 | { 22 | "go.googlesource.com/text", 23 | "github.com/golang/text", 24 | }, 25 | } 26 | 27 | for _, tt := range cases { 28 | t.Run(tt.Input, func(t *testing.T) { 29 | var tr Translator 30 | actual, ok := tr.Translate(context.Background(), module.Module{ 31 | Path: tt.Input, 32 | }) 33 | 34 | if tt.Output == "" { 35 | require.False(t, ok) 36 | return 37 | } 38 | 39 | require.True(t, ok) 40 | require.Equal(t, tt.Output, actual.Path) 41 | }) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /license/gopkg/translate.go: -------------------------------------------------------------------------------- 1 | package gopkg 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "regexp" 7 | 8 | "github.com/mitchellh/golicense/module" 9 | ) 10 | 11 | type Translator struct{} 12 | 13 | func (t Translator) Translate(ctx context.Context, m module.Module) (module.Module, bool) { 14 | ms := re.FindStringSubmatch(m.Path) 15 | if ms == nil { 16 | return module.Module{}, false 17 | } 18 | 19 | // URL case 1 with no user means it is go- 20 | if ms[1] == "" { 21 | ms[1] = "go-" + ms[2] 22 | } 23 | 24 | // Matches, convert to github 25 | m.Path = fmt.Sprintf("github.com/%s/%s", ms[1], ms[2]) 26 | return m, true 27 | } 28 | 29 | // re is the regexp matching the package for a GoPkg import. This is taken 30 | // almost directly from the GoPkg source code itself so it should match 31 | // perfectly. 32 | var re = regexp.MustCompile(`(?i)^gopkg\.in/(?:([a-zA-Z0-9][-a-zA-Z0-9]+)/)?([a-zA-Z][-.a-zA-Z0-9]*)\.((?:v0|v[1-9][0-9]*)(?:\.0|\.[1-9][0-9]*){0,2}(?:-unstable)?)(?:\.git)?((?:/[a-zA-Z0-9][-.a-zA-Z0-9]*)*)$`) 33 | -------------------------------------------------------------------------------- /license/gopkg/translate_test.go: -------------------------------------------------------------------------------- 1 | package gopkg 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/mitchellh/golicense/module" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestTranslator(t *testing.T) { 12 | cases := []struct { 13 | Input string 14 | Output string 15 | }{ 16 | { 17 | "github.com/foo/bar", 18 | "", 19 | }, 20 | 21 | { 22 | "gopkg.in/pkg.v3", 23 | "github.com/go-pkg/pkg", 24 | }, 25 | 26 | { 27 | "gopkg.in/yaml.v3", 28 | "github.com/go-yaml/yaml", 29 | }, 30 | 31 | { 32 | "gopkg.in/mitchellh/foo.v22", 33 | "github.com/mitchellh/foo", 34 | }, 35 | } 36 | 37 | for _, tt := range cases { 38 | t.Run(tt.Input, func(t *testing.T) { 39 | var tr Translator 40 | actual, ok := tr.Translate(context.Background(), module.Module{ 41 | Path: tt.Input, 42 | }) 43 | 44 | if tt.Output == "" { 45 | require.False(t, ok) 46 | return 47 | } 48 | 49 | require.True(t, ok) 50 | require.Equal(t, tt.Output, actual.Path) 51 | }) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /license/license.go: -------------------------------------------------------------------------------- 1 | package license 2 | 3 | //go:generate mockery -all -inpkg 4 | 5 | // License represents a software license. 6 | type License struct { 7 | Name string // Name is a human-friendly name like "MIT License" 8 | SPDX string // SPDX ID of the license, blank if unknown or unavailable 9 | } 10 | 11 | func (l *License) String() string { 12 | if l == nil { 13 | return "" 14 | } 15 | 16 | return l.Name 17 | } 18 | -------------------------------------------------------------------------------- /license/mapper/finder.go: -------------------------------------------------------------------------------- 1 | package mapper 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/mitchellh/go-spdx" 8 | "github.com/mitchellh/golicense/license" 9 | "github.com/mitchellh/golicense/module" 10 | ) 11 | 12 | // Finder implements license.Finder and sets the license type based on the 13 | // given mapping if the path exists in the map. 14 | type Finder struct { 15 | Map map[string]string 16 | } 17 | 18 | // License implements license.Finder 19 | func (f *Finder) License(ctx context.Context, m module.Module) (*license.License, error) { 20 | v, ok := f.Map[m.Path] 21 | if !ok { 22 | return nil, nil 23 | } 24 | 25 | // Look up the license by SPDX ID 26 | lic, err := spdx.License(v) 27 | if err != nil { 28 | return nil, fmt.Errorf("Override license %q SPDX lookup error: %s", v, err) 29 | } 30 | 31 | return &license.License{Name: lic.Name, SPDX: lic.ID}, nil 32 | } 33 | -------------------------------------------------------------------------------- /license/mapper/translate.go: -------------------------------------------------------------------------------- 1 | // Package mapper contains a translator using a raw map[string]string 2 | package mapper 3 | 4 | import ( 5 | "context" 6 | "fmt" 7 | "regexp" 8 | "strings" 9 | 10 | "github.com/mitchellh/golicense/module" 11 | ) 12 | 13 | type Translator struct { 14 | // Map is the mapping of package names to translate. If the name is 15 | // exact then it will map exactly to the destination. If the name begins 16 | // and ends with `/` (forward slash) then it will be treated like a regular 17 | // expression. The destination can use \1, \2, ... to reference capture 18 | // groups. 19 | // 20 | // The translation will run until in a loop until no translation occurs 21 | // anymore or len(Map) translations occur, in which case it is an error. 22 | Map map[string]string 23 | } 24 | 25 | func (t Translator) Translate(ctx context.Context, m module.Module) (module.Module, bool) { 26 | count := 0 27 | 28 | RESTART: 29 | if count > len(t.Map) { 30 | // No way to error currently... 31 | return module.Module{}, false 32 | } 33 | 34 | for k, v := range t.Map { 35 | if k == m.Path { 36 | m.Path = v 37 | count++ 38 | goto RESTART 39 | } 40 | 41 | if k[0] == '/' && k[len(k)-1] == '/' { 42 | // Note that this isn't super performant since we constantly 43 | // recompile any translations as we retry, but we don't expect 44 | // many translations. If this ever becomes a performance issue, 45 | // we can fix it then. 46 | re, err := regexp.Compile(k[1 : len(k)-1]) 47 | if err != nil { 48 | return module.Module{}, false 49 | } 50 | 51 | ms := re.FindStringSubmatch(m.Path) 52 | if ms == nil { 53 | continue 54 | } 55 | 56 | for i, m := range ms { 57 | v = strings.Replace(v, fmt.Sprintf("\\%d", i), m, -1) 58 | } 59 | 60 | m.Path = v 61 | count++ 62 | goto RESTART 63 | } 64 | } 65 | 66 | return m, count > 0 67 | } 68 | -------------------------------------------------------------------------------- /license/mapper/translate_test.go: -------------------------------------------------------------------------------- 1 | package mapper 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/mitchellh/golicense/module" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestTranslator(t *testing.T) { 12 | cases := []struct { 13 | Map map[string]string 14 | Input string 15 | Output string 16 | }{ 17 | { 18 | nil, 19 | "github.com/foo/bar", 20 | "", 21 | }, 22 | 23 | { 24 | map[string]string{ 25 | "gopkg.in/pkg.v3": "github.com/go-pkg/pkg", 26 | }, 27 | "gopkg.in/pkg.v3", 28 | "github.com/go-pkg/pkg", 29 | }, 30 | 31 | { 32 | map[string]string{ 33 | `/^gopkg\.in/([^/]+)/([^/]+)\./`: `github.com/\1/\2`, 34 | }, 35 | "gopkg.in/mitchellh/foo.v22", 36 | "github.com/mitchellh/foo", 37 | }, 38 | } 39 | 40 | for _, tt := range cases { 41 | t.Run(tt.Input, func(t *testing.T) { 42 | tr := &Translator{Map: tt.Map} 43 | actual, ok := tr.Translate(context.Background(), module.Module{ 44 | Path: tt.Input, 45 | }) 46 | 47 | if tt.Output == "" { 48 | require.False(t, ok) 49 | return 50 | } 51 | 52 | require.True(t, ok) 53 | require.Equal(t, tt.Output, actual.Path) 54 | }) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /license/mock_Finder.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.0.0. DO NOT EDIT. 2 | 3 | package license 4 | 5 | import context "context" 6 | import mock "github.com/stretchr/testify/mock" 7 | import module "github.com/mitchellh/golicense/module" 8 | 9 | // MockFinder is an autogenerated mock type for the Finder type 10 | type MockFinder struct { 11 | mock.Mock 12 | } 13 | 14 | // License provides a mock function with given fields: _a0, _a1 15 | func (_m *MockFinder) License(_a0 context.Context, _a1 module.Module) (*License, error) { 16 | ret := _m.Called(_a0, _a1) 17 | 18 | var r0 *License 19 | if rf, ok := ret.Get(0).(func(context.Context, module.Module) *License); ok { 20 | r0 = rf(_a0, _a1) 21 | } else { 22 | if ret.Get(0) != nil { 23 | r0 = ret.Get(0).(*License) 24 | } 25 | } 26 | 27 | var r1 error 28 | if rf, ok := ret.Get(1).(func(context.Context, module.Module) error); ok { 29 | r1 = rf(_a0, _a1) 30 | } else { 31 | r1 = ret.Error(1) 32 | } 33 | 34 | return r0, r1 35 | } 36 | -------------------------------------------------------------------------------- /license/mock_StatusListener.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.0.0. DO NOT EDIT. 2 | 3 | package license 4 | 5 | import mock "github.com/stretchr/testify/mock" 6 | 7 | // MockStatusListener is an autogenerated mock type for the StatusListener type 8 | type MockStatusListener struct { 9 | mock.Mock 10 | } 11 | 12 | // UpdateStatus provides a mock function with given fields: t, msg 13 | func (_m *MockStatusListener) UpdateStatus(t StatusType, msg string) { 14 | _m.Called(t, msg) 15 | } 16 | -------------------------------------------------------------------------------- /license/resolver/translate.go: -------------------------------------------------------------------------------- 1 | package resolver 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "regexp" 7 | 8 | "github.com/mitchellh/golicense/license" 9 | "github.com/mitchellh/golicense/module" 10 | "golang.org/x/tools/go/vcs" 11 | ) 12 | 13 | // Translator resolves import paths to their proper VCS location. For 14 | // example: "rsc.io/pdf" turns into "github.com/rsc/pdf". 15 | type Translator struct{} 16 | 17 | func (t Translator) Translate(ctx context.Context, m module.Module) (module.Module, bool) { 18 | root, err := vcs.RepoRootForImportPath(m.Path, false) 19 | if err != nil { 20 | return module.Module{}, false 21 | } 22 | 23 | path := hostStripRe.ReplaceAllString(root.Repo, "") 24 | if m.Path == path { 25 | return module.Module{}, false 26 | } 27 | 28 | license.UpdateStatus(ctx, license.StatusNormal, fmt.Sprintf( 29 | "translated %q to %q", m.Path, path)) 30 | m.Path = path 31 | return m, true 32 | } 33 | 34 | // hostStripRe is a simple regexp to strip the schema from a URL. 35 | var hostStripRe = regexp.MustCompile(`^\w+:\/\/`) 36 | -------------------------------------------------------------------------------- /license/resolver/translate_test.go: -------------------------------------------------------------------------------- 1 | package resolver 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/mitchellh/golicense/module" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestTranslator(t *testing.T) { 12 | cases := []struct { 13 | Input string 14 | Output string 15 | }{ 16 | { 17 | "github.com/foo/bar", 18 | "", 19 | }, 20 | 21 | { 22 | "golang.org/x/text", 23 | "go.googlesource.com/text", 24 | }, 25 | 26 | { 27 | "gonum.org/v1/gonum", 28 | "github.com/gonum/gonum", 29 | }, 30 | } 31 | 32 | for _, tt := range cases { 33 | t.Run(tt.Input, func(t *testing.T) { 34 | var tr Translator 35 | actual, ok := tr.Translate(context.Background(), module.Module{ 36 | Path: tt.Input, 37 | }) 38 | 39 | if tt.Output == "" { 40 | require.False(t, ok) 41 | return 42 | } 43 | 44 | require.True(t, ok) 45 | require.Equal(t, tt.Output, actual.Path) 46 | }) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /license/status.go: -------------------------------------------------------------------------------- 1 | package license 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | // StatusType is the type of status message, such as normal, error, warning. 8 | type StatusType uint 9 | 10 | const ( 11 | StatusUnknown StatusType = iota 12 | StatusNormal 13 | StatusWarning 14 | StatusError 15 | ) 16 | 17 | // StatusListener is called to update the status of a finder. 18 | type StatusListener interface { 19 | // UpdateStatus is called whenever there is an updated status message. 20 | // This function must not block, since this will block the actual 21 | // behavior of the license finder as well. If blocking behavior is 22 | // necessary, end users should use a channel internally to avoid it on 23 | // the function call. 24 | // 25 | // The message should be relatively short if possible (within ~50 chars) 26 | // so that it fits nicely on a terminal. It should be a basic status 27 | // update. 28 | UpdateStatus(t StatusType, msg string) 29 | } 30 | 31 | // StatusWithContext inserts a StatusListener into a context. 32 | func StatusWithContext(ctx context.Context, l StatusListener) context.Context { 33 | return context.WithValue(ctx, statusCtxKey, l) 34 | } 35 | 36 | // UpdateStatus updates the status of the listener (if any) in the given 37 | // context. 38 | func UpdateStatus(ctx context.Context, t StatusType, msg string) { 39 | sl, ok := ctx.Value(statusCtxKey).(StatusListener) 40 | if !ok || sl == nil { 41 | return 42 | } 43 | 44 | sl.UpdateStatus(t, msg) 45 | } 46 | 47 | type statusCtxKeyType struct{} 48 | 49 | var statusCtxKey = statusCtxKeyType{} 50 | -------------------------------------------------------------------------------- /license/status_test.go: -------------------------------------------------------------------------------- 1 | package license 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | ) 7 | 8 | func TestUpdateStatus_noExist(t *testing.T) { 9 | // Just basically testing that we don't panic here 10 | UpdateStatus(context.Background(), StatusNormal, "hello") 11 | } 12 | 13 | func TestUpdateStatus_badType(t *testing.T) { 14 | // Just basically testing that we don't panic here 15 | UpdateStatus(context.WithValue(context.Background(), statusCtxKey, 42), 16 | StatusNormal, "hello") 17 | } 18 | 19 | func TestUpdateStatus_good(t *testing.T) { 20 | var mock MockStatusListener 21 | ctx := StatusWithContext(context.Background(), &mock) 22 | 23 | mock.On("UpdateStatus", StatusNormal, "hello").Once() 24 | UpdateStatus(ctx, StatusNormal, "hello") 25 | mock.AssertExpectations(t) 26 | 27 | mock.On("UpdateStatus", StatusWarning, "warning").Once() 28 | UpdateStatus(ctx, StatusWarning, "warning") 29 | mock.AssertExpectations(t) 30 | } 31 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "net/http" 8 | "os" 9 | "strings" 10 | "sync" 11 | 12 | "github.com/fatih/color" 13 | "github.com/google/go-github/v18/github" 14 | "github.com/rsc/goversion/version" 15 | "golang.org/x/oauth2" 16 | 17 | "github.com/mitchellh/golicense/config" 18 | "github.com/mitchellh/golicense/license" 19 | githubFinder "github.com/mitchellh/golicense/license/github" 20 | "github.com/mitchellh/golicense/license/golang" 21 | "github.com/mitchellh/golicense/license/gopkg" 22 | "github.com/mitchellh/golicense/license/mapper" 23 | "github.com/mitchellh/golicense/license/resolver" 24 | "github.com/mitchellh/golicense/module" 25 | ) 26 | 27 | const ( 28 | EnvGitHubToken = "GITHUB_TOKEN" 29 | ) 30 | 31 | func main() { 32 | os.Exit(realMain()) 33 | } 34 | 35 | func realMain() int { 36 | termOut := &TermOutput{Out: os.Stdout} 37 | 38 | var flagLicense bool 39 | var flagOutXLSX string 40 | flags := flag.NewFlagSet(os.Args[0], flag.ExitOnError) 41 | flags.BoolVar(&flagLicense, "license", true, 42 | "look up and verify license. If false, dependencies are\n"+ 43 | "printed without licenses.") 44 | flags.BoolVar(&termOut.Plain, "plain", false, "plain terminal output, no colors or live updates") 45 | flags.BoolVar(&termOut.Verbose, "verbose", false, "additional logging to terminal, requires -plain") 46 | flags.StringVar(&flagOutXLSX, "out-xlsx", "", 47 | "save report in Excel XLSX format to the given path") 48 | flags.Parse(os.Args[1:]) 49 | args := flags.Args() 50 | if len(args) == 0 { 51 | fmt.Fprintf(os.Stderr, color.RedString( 52 | "❗️ Path to file to analyze expected.\n\n")) 53 | printHelp(flags) 54 | return 1 55 | } 56 | 57 | // Determine the exe path and parse the configuration if given. 58 | var cfg config.Config 59 | exePaths := args[:1] 60 | if len(args) > 1 { 61 | exePaths = args[1:] 62 | 63 | c, err := config.ParseFile(args[0]) 64 | if err != nil { 65 | fmt.Fprintf(os.Stderr, color.RedString(fmt.Sprintf( 66 | "❗️ Error parsing configuration:\n\n%s\n", err))) 67 | return 1 68 | } 69 | 70 | // Store the config and set it on the output 71 | cfg = *c 72 | } 73 | 74 | allMods := map[module.Module]struct{}{} 75 | for _, exePath := range exePaths { 76 | // Read the dependencies from the binary itself 77 | vsn, err := version.ReadExe(exePath) 78 | if err != nil { 79 | fmt.Fprintf(os.Stderr, color.RedString(fmt.Sprintf( 80 | "❗️ Error reading %q: %s\n", args[0], err))) 81 | return 1 82 | } 83 | 84 | if vsn.ModuleInfo == "" { 85 | // ModuleInfo empty means that the binary didn't use Go modules 86 | // or it could mean that a binary has no dependencies. Either way 87 | // we error since we can't be sure. 88 | fmt.Fprintf(os.Stderr, color.YellowString(fmt.Sprintf( 89 | "⚠️ %q ⚠️\n\n"+ 90 | "This executable was compiled without using Go modules or has \n"+ 91 | "zero dependencies. golicense considers this an error (exit code 1).\n", exePath))) 92 | return 1 93 | } 94 | 95 | // From the raw module string from the binary, we need to parse this 96 | // into structured data with the module information. 97 | mods, err := module.ParseExeData(vsn.ModuleInfo) 98 | if err != nil { 99 | fmt.Fprintf(os.Stderr, color.RedString(fmt.Sprintf( 100 | "❗️ Error parsing dependencies: %s\n", err))) 101 | return 1 102 | } 103 | for _, mod := range mods { 104 | allMods[mod] = struct{}{} 105 | } 106 | } 107 | 108 | mods := make([]module.Module, 0, len(allMods)) 109 | for mod := range allMods { 110 | mods = append(mods, mod) 111 | } 112 | 113 | // Complete terminal output setup 114 | termOut.Config = &cfg 115 | termOut.Modules = mods 116 | 117 | // Setup the outputs 118 | out := &MultiOutput{Outputs: []Output{termOut}} 119 | if flagOutXLSX != "" { 120 | out.Outputs = append(out.Outputs, &XLSXOutput{ 121 | Path: flagOutXLSX, 122 | Config: &cfg, 123 | }) 124 | } 125 | 126 | // Setup a context. We don't connect this to an interrupt signal or 127 | // anything since we just exit immediately on interrupt. No cleanup 128 | // necessary. 129 | ctx := context.Background() 130 | 131 | // Auth with GitHub if available 132 | var githubClient *http.Client 133 | if v := os.Getenv(EnvGitHubToken); v != "" { 134 | ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: v}) 135 | githubClient = oauth2.NewClient(ctx, ts) 136 | } 137 | 138 | // Build our translators and license finders 139 | ts := []license.Translator{ 140 | &mapper.Translator{Map: cfg.Translate}, 141 | &resolver.Translator{}, 142 | &golang.Translator{}, 143 | &gopkg.Translator{}, 144 | } 145 | var fs []license.Finder 146 | if flagLicense { 147 | fs = []license.Finder{ 148 | &mapper.Finder{Map: cfg.Override}, 149 | &githubFinder.RepoAPI{ 150 | Client: github.NewClient(githubClient), 151 | }, 152 | } 153 | } 154 | 155 | // Kick off all the license lookups. 156 | var wg sync.WaitGroup 157 | sem := NewSemaphore(5) 158 | for _, m := range mods { 159 | wg.Add(1) 160 | go func(m module.Module) { 161 | defer wg.Done() 162 | 163 | // Acquire a semaphore so that we can limit concurrency 164 | sem.Acquire() 165 | defer sem.Release() 166 | 167 | // Build the context 168 | ctx := license.StatusWithContext(ctx, StatusListener(out, &m)) 169 | 170 | // Lookup 171 | out.Start(&m) 172 | 173 | // We first try the untranslated version. If we can detect 174 | // a license then take that. Otherwise, we translate. 175 | lic, err := license.Find(ctx, m, fs) 176 | if lic == nil || err != nil { 177 | lic, err = license.Find(ctx, license.Translate(ctx, m, ts), fs) 178 | } 179 | out.Finish(&m, lic, err) 180 | }(m) 181 | } 182 | 183 | // Wait for all lookups to complete 184 | wg.Wait() 185 | 186 | // Close the output 187 | if err := out.Close(); err != nil { 188 | fmt.Fprintf(os.Stderr, color.RedString(fmt.Sprintf( 189 | "❗️ Error: %s\n", err))) 190 | return 1 191 | } 192 | 193 | return termOut.ExitCode() 194 | } 195 | 196 | func printHelp(fs *flag.FlagSet) { 197 | fmt.Fprintf(os.Stderr, strings.TrimSpace(help)+"\n\n", os.Args[0]) 198 | fs.PrintDefaults() 199 | } 200 | 201 | const help = ` 202 | golicense analyzes the dependencies of a binary compiled from Go. 203 | 204 | Usage: %[1]s [flags] [BINARY] 205 | Usage: %[1]s [flags] [CONFIG] [BINARY] 206 | 207 | One or two arguments can be given: a binary by itself which will output 208 | all the licenses of dependencies, or a configuration file and a binary 209 | which also notes which licenses are allowed among other settings. 210 | 211 | For full help text, see the README in the GitHub repository: 212 | http://github.com/mitchellh/golicense 213 | 214 | Flags: 215 | 216 | ` 217 | -------------------------------------------------------------------------------- /module/module.go: -------------------------------------------------------------------------------- 1 | package module 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strings" 7 | ) 8 | 9 | // Module represents a single Go module. 10 | // 11 | // Depending on the source that this is parsed from, fields may be empty. 12 | // All helper functions on Module work with zero values. See their associated 13 | // documentation for more information on exact behavior. 14 | type Module struct { 15 | Path string // Import path, such as "github.com/mitchellh/golicense" 16 | Version string // Version like "v1.2.3" 17 | Hash string // Hash such as "h1:abcd1234" 18 | } 19 | 20 | // String returns a human readable string format. 21 | func (m *Module) String() string { 22 | return fmt.Sprintf("%s (%s)", m.Path, m.Version) 23 | } 24 | 25 | // ParseExeData parses the raw dependency information from a compiled Go 26 | // binary's readonly data section. Any unexpected values will return errors. 27 | func ParseExeData(raw string) ([]Module, error) { 28 | var result []Module 29 | for _, line := range strings.Split(strings.TrimSpace(raw), "\n") { 30 | row := strings.Split(line, "\t") 31 | 32 | // Ignore non-dependency information, such as path/mod. The 33 | // "=>" syntax means it is a replacement. 34 | if row[0] != "dep" && row[0] != "=>" { 35 | continue 36 | } 37 | 38 | if len(row) == 3 { 39 | // A row with 3 can occur if there is no hash data for the 40 | // dependency. 41 | row = append(row, "") 42 | } 43 | 44 | if len(row) != 4 { 45 | return nil, fmt.Errorf( 46 | "Unexpected raw dependency format: %s", line) 47 | } 48 | 49 | // If the path ends in an import version, strip it since we have 50 | // an exact version available in Version. 51 | if loc := importVersionRe.FindStringIndex(row[1]); loc != nil { 52 | row[1] = row[1][:loc[0]] 53 | } 54 | 55 | next := Module{ 56 | Path: row[1], 57 | Version: row[2], 58 | Hash: row[3], 59 | } 60 | 61 | // If this is a replacement, then replace the last result 62 | if row[0] == "=>" { 63 | result[len(result)-1] = next 64 | continue 65 | } 66 | 67 | result = append(result, next) 68 | } 69 | 70 | return result, nil 71 | } 72 | 73 | // importVersionRe is a regular expression that matches the trailing 74 | // import version specifiers like `/v12` on an import that is Go modules 75 | // compatible. 76 | var importVersionRe = regexp.MustCompile(`/v\d+$`) 77 | -------------------------------------------------------------------------------- /module/module_test.go: -------------------------------------------------------------------------------- 1 | package module 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestParseExeData(t *testing.T) { 11 | cases := []struct { 12 | Name string 13 | Input string 14 | Expected []Module 15 | Error string 16 | }{ 17 | { 18 | "typical (from golicense itself)", 19 | testExeData, 20 | []Module{ 21 | Module{ 22 | Path: "github.com/fatih/color", 23 | Version: "v1.7.0", 24 | Hash: "h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=", 25 | }, 26 | Module{ 27 | Path: "github.com/mattn/go-colorable", 28 | Version: "v0.0.9", 29 | Hash: "h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=", 30 | }, 31 | Module{ 32 | Path: "github.com/mattn/go-isatty", 33 | Version: "v0.0.4", 34 | Hash: "h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs=", 35 | }, 36 | Module{ 37 | Path: "github.com/rsc/goversion", 38 | Version: "v1.2.0", 39 | Hash: "h1:zVF4y5ciA/rw779S62bEAq4Yif1cBc/UwRkXJ2xZyT4=", 40 | }, 41 | Module{ 42 | Path: "github.com/rsc/goversion", 43 | Version: "v12.0.0", 44 | Hash: "h1:zVF4y5ciA/rw779S62bEAq4Yif1cBc/UwRkXJ2xZyT4=", 45 | }, 46 | }, 47 | "", 48 | }, 49 | 50 | { 51 | "replacement syntax", 52 | strings.TrimSpace(replacement), 53 | []Module{ 54 | Module{ 55 | Path: "github.com/markbates/inflect", 56 | Version: "v0.0.0-20171215194931-a12c3aec81a6", 57 | Hash: "h1:LZhVjIISSbj8qLf2qDPP0D8z0uvOWAW5C85ly5mJW6c=", 58 | }, 59 | }, 60 | "", 61 | }, 62 | } 63 | 64 | for _, tt := range cases { 65 | t.Run(tt.Name, func(t *testing.T) { 66 | require := require.New(t) 67 | actual, err := ParseExeData(tt.Input) 68 | if tt.Error != "" { 69 | require.Error(err) 70 | require.Contains(err.Error(), tt.Error) 71 | return 72 | } 73 | require.NoError(err) 74 | require.Equal(tt.Expected, actual) 75 | }) 76 | } 77 | } 78 | 79 | const testExeData = "path\tgithub.com/mitchellh/golicense\nmod\tgithub.com/mitchellh/golicense\t(devel)\t\ndep\tgithub.com/fatih/color\tv1.7.0\th1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=\ndep\tgithub.com/mattn/go-colorable\tv0.0.9\th1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=\ndep\tgithub.com/mattn/go-isatty\tv0.0.4\th1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs=\ndep\tgithub.com/rsc/goversion\tv1.2.0\th1:zVF4y5ciA/rw779S62bEAq4Yif1cBc/UwRkXJ2xZyT4=\ndep\tgithub.com/rsc/goversion/v12\tv12.0.0\th1:zVF4y5ciA/rw779S62bEAq4Yif1cBc/UwRkXJ2xZyT4=\n" 80 | 81 | const replacement = ` 82 | path github.com/gohugoio/hugo 83 | mod github.com/gohugoio/hugo (devel) 84 | dep github.com/markbates/inflect v1.0.0 85 | => github.com/markbates/inflect v0.0.0-20171215194931-a12c3aec81a6 h1:LZhVjIISSbj8qLf2qDPP0D8z0uvOWAW5C85ly5mJW6c= 86 | ` 87 | -------------------------------------------------------------------------------- /module/sort.go: -------------------------------------------------------------------------------- 1 | package module 2 | 3 | // SortByPath implements sort.Interface to sort a slice of Module by path. 4 | type SortByPath []Module 5 | 6 | func (s SortByPath) Len() int { return len(s) } 7 | func (s SortByPath) Swap(i, j int) { s[i], s[j] = s[j], s[i] } 8 | func (s SortByPath) Less(i, j int) bool { return s[i].Path < s[j].Path } 9 | -------------------------------------------------------------------------------- /module/sort_test.go: -------------------------------------------------------------------------------- 1 | package module 2 | 3 | import ( 4 | "sort" 5 | "testing" 6 | ) 7 | 8 | func TestSort_interface(t *testing.T) { 9 | var _ sort.Interface = SortByPath(nil) 10 | } 11 | -------------------------------------------------------------------------------- /output.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/mitchellh/golicense/license" 5 | "github.com/mitchellh/golicense/module" 6 | ) 7 | 8 | // Output represents the output format for the progress and completion 9 | // of license lookups. This can be implemented to introduce new UI styles 10 | // or output formats (like JSON, etc.). 11 | type Output interface { 12 | // Start is called when the license lookup for a module is started. 13 | Start(*module.Module) 14 | 15 | // Update is called for each status update during the license lookup. 16 | Update(*module.Module, license.StatusType, string) 17 | 18 | // Finish is called when a module license lookup is complete with 19 | // the results of the lookup. 20 | Finish(*module.Module, *license.License, error) 21 | 22 | // Close is called when all modules lookups are completed. This can be 23 | // used to output a summary report, if any. 24 | Close() error 25 | } 26 | 27 | // StatusListener returns a license.StatusListener implementation for 28 | // a single module to route to an Output implementation. 29 | // 30 | // The caller must still call Start and Finish appropriately on the 31 | // Output while using this StatusListener. 32 | func StatusListener(o Output, m *module.Module) license.StatusListener { 33 | return &outputStatusListener{m: m, o: o} 34 | } 35 | 36 | // outputStatusListener is a license.StatusListener implementation that 37 | // updates the Output for a single module by calling Update. 38 | type outputStatusListener struct { 39 | m *module.Module 40 | o Output 41 | } 42 | 43 | // UpdateStatus implements license.StatusListener 44 | func (sl *outputStatusListener) UpdateStatus(t license.StatusType, msg string) { 45 | sl.o.Update(sl.m, t, msg) 46 | } 47 | -------------------------------------------------------------------------------- /output_multi.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/hashicorp/go-multierror" 5 | "github.com/mitchellh/golicense/license" 6 | "github.com/mitchellh/golicense/module" 7 | ) 8 | 9 | // MultiOutput calls the functions of multiple Output implementations. 10 | type MultiOutput struct { 11 | Outputs []Output 12 | } 13 | 14 | // Start implements Output 15 | func (o *MultiOutput) Start(m *module.Module) { 16 | for _, out := range o.Outputs { 17 | out.Start(m) 18 | } 19 | } 20 | 21 | // Update implements Output 22 | func (o *MultiOutput) Update(m *module.Module, t license.StatusType, msg string) { 23 | for _, out := range o.Outputs { 24 | out.Update(m, t, msg) 25 | } 26 | } 27 | 28 | // Finish implements Output 29 | func (o *MultiOutput) Finish(m *module.Module, l *license.License, err error) { 30 | for _, out := range o.Outputs { 31 | out.Finish(m, l, err) 32 | } 33 | } 34 | 35 | // Close implements Output 36 | func (o *MultiOutput) Close() error { 37 | var err error 38 | for _, out := range o.Outputs { 39 | if e := out.Close(); e != nil { 40 | err = multierror.Append(err, e) 41 | } 42 | } 43 | 44 | return err 45 | } 46 | -------------------------------------------------------------------------------- /output_terminal.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "sort" 8 | "strings" 9 | "sync" 10 | 11 | "github.com/fatih/color" 12 | "github.com/gosuri/uilive" 13 | "golang.org/x/crypto/ssh/terminal" 14 | 15 | "github.com/mitchellh/golicense/config" 16 | "github.com/mitchellh/golicense/license" 17 | "github.com/mitchellh/golicense/module" 18 | ) 19 | 20 | // TermOutput is an Output implementation that outputs to the terminal. 21 | type TermOutput struct { 22 | // Out is the stdout to write to. If this is a TTY, TermOutput will 23 | // automatically use a "live" updating output mode for status updates. 24 | // This can be disabled by setting Plain to true below. 25 | Out io.Writer 26 | 27 | // Config is the configuration (if any). This will be used to check 28 | // if a license is allowed or not. 29 | Config *config.Config 30 | 31 | // Modules is the full list of modules that will be checked. This is 32 | // optional. If this is given in advance, then the output will be cleanly 33 | // aligned. 34 | Modules []module.Module 35 | 36 | // Plain, if true, will use the plain output vs the live updating output. 37 | // TermOutput will always use Plain output if the Out configured above 38 | // is not a TTY. 39 | Plain bool 40 | 41 | // Verbose will log all status updates in plain mode. This has no effect 42 | // in non-plain mode currently. 43 | Verbose bool 44 | 45 | modules map[string]string 46 | moduleMax int 47 | exitCode int 48 | lineMax int 49 | live *uilive.Writer 50 | once sync.Once 51 | lock sync.Mutex 52 | } 53 | 54 | func (o *TermOutput) ExitCode() int { 55 | return o.exitCode 56 | } 57 | 58 | // Start implements Output 59 | func (o *TermOutput) Start(m *module.Module) { 60 | o.once.Do(o.init) 61 | 62 | if o.Plain { 63 | return 64 | } 65 | 66 | o.lock.Lock() 67 | defer o.lock.Unlock() 68 | o.modules[m.Path] = fmt.Sprintf("%s %s starting...", iconNormal, o.paddedModule(m)) 69 | o.updateLiveOutput() 70 | } 71 | 72 | // Update implements Output 73 | func (o *TermOutput) Update(m *module.Module, t license.StatusType, msg string) { 74 | o.once.Do(o.init) 75 | 76 | // In plain & verbose mode, we output every status message, but in normal 77 | // plain mode we ignore all status updates. 78 | if o.Plain && o.Verbose { 79 | fmt.Fprintf(o.Out, fmt.Sprintf( 80 | "%s %s\n", o.paddedModule(m), msg)) 81 | } 82 | 83 | if o.Plain { 84 | return 85 | } 86 | 87 | var colorFunc func(string, ...interface{}) string = fmt.Sprintf 88 | icon := iconNormal 89 | switch t { 90 | case license.StatusWarning: 91 | icon = iconWarning 92 | colorFunc = color.YellowString 93 | 94 | case license.StatusError: 95 | icon = iconError 96 | colorFunc = color.RedString 97 | } 98 | if icon != "" { 99 | icon += " " 100 | } 101 | 102 | o.lock.Lock() 103 | defer o.lock.Unlock() 104 | o.modules[m.Path] = colorFunc("%s%s %s", icon, o.paddedModule(m), msg) 105 | o.updateLiveOutput() 106 | } 107 | 108 | // Finish implements Output 109 | func (o *TermOutput) Finish(m *module.Module, l *license.License, err error) { 110 | o.once.Do(o.init) 111 | 112 | var colorFunc func(string, ...interface{}) string = fmt.Sprintf 113 | icon := iconNormal 114 | if o.Config != nil { 115 | state := o.Config.Allowed(l) 116 | switch state { 117 | case config.StateAllowed: 118 | colorFunc = color.GreenString 119 | icon = iconSuccess 120 | 121 | case config.StateDenied: 122 | colorFunc = color.RedString 123 | icon = iconError 124 | o.exitCode = 1 125 | 126 | case config.StateUnknown: 127 | if len(o.Config.Allow) > 0 || len(o.Config.Deny) > 0 { 128 | colorFunc = color.YellowString 129 | icon = iconWarning 130 | o.exitCode = 1 131 | } 132 | } 133 | } 134 | if icon != "" { 135 | icon += " " 136 | } 137 | 138 | if o.Plain { 139 | fmt.Fprintf(o.Out, fmt.Sprintf( 140 | "%s %s\n", o.paddedModule(m), l.String())) 141 | return 142 | } 143 | 144 | o.lock.Lock() 145 | defer o.lock.Unlock() 146 | delete(o.modules, m.Path) 147 | o.pauseLive(func() { 148 | o.live.Write([]byte(colorFunc( 149 | "%s%s %s\n", icon, o.paddedModule(m), l.String()))) 150 | }) 151 | } 152 | 153 | // Close implements Output 154 | func (o *TermOutput) Close() error { 155 | o.lock.Lock() 156 | defer o.lock.Unlock() 157 | 158 | if o.live != nil { 159 | o.live.Stop() 160 | } 161 | 162 | return nil 163 | } 164 | 165 | // paddedModule returns the name of the module padded so that they align nicely. 166 | func (o *TermOutput) paddedModule(m *module.Module) string { 167 | o.once.Do(o.init) 168 | 169 | if o.moduleMax == 0 { 170 | return m.Path 171 | } 172 | 173 | // Pad the path so that it is equivalent to the moduleMax length 174 | return m.Path + strings.Repeat(" ", o.moduleMax-len(m.Path)) 175 | } 176 | 177 | // pauseLive pauses the live output for the duration of the function. 178 | // 179 | // lock must be held. 180 | func (o *TermOutput) pauseLive(f func()) { 181 | o.live.Write([]byte(strings.Repeat(" ", o.lineMax) + "\n")) 182 | o.live.Flush() 183 | f() 184 | o.live.Flush() 185 | o.live.Stop() 186 | o.newLive() 187 | o.updateLiveOutput() 188 | } 189 | 190 | // updateLiveOutput updates the output buffer for live status. 191 | // 192 | // lock must be held when this is called 193 | func (o *TermOutput) updateLiveOutput() { 194 | keys := make([]string, 0, len(o.modules)) 195 | for k := range o.modules { 196 | keys = append(keys, k) 197 | } 198 | sort.Strings(keys) 199 | 200 | var buf bytes.Buffer 201 | for _, k := range keys { 202 | if v := len(o.modules[k]); v > o.lineMax { 203 | o.lineMax = v 204 | } 205 | 206 | buf.WriteString(o.modules[k] + strings.Repeat(" ", o.lineMax-len(o.modules[k])) + "\n") 207 | } 208 | 209 | o.live.Write(buf.Bytes()) 210 | o.live.Flush() 211 | } 212 | 213 | func (o *TermOutput) newLive() { 214 | o.live = uilive.New() 215 | o.live.Out = o.Out 216 | o.live.Start() 217 | } 218 | 219 | func (o *TermOutput) init() { 220 | if o.modules == nil { 221 | o.modules = make(map[string]string) 222 | } 223 | 224 | // Calculate the maximum module length 225 | for _, m := range o.Modules { 226 | if v := len(m.Path); v > o.moduleMax { 227 | o.moduleMax = v 228 | } 229 | } 230 | 231 | // Check if the output is a TTY 232 | if !o.Plain { 233 | o.Plain = true // default to plain mode unless we can verify TTY 234 | if iofd, ok := o.Out.(ioFd); ok { 235 | o.Plain = !terminal.IsTerminal(int(iofd.Fd())) 236 | } 237 | 238 | if !o.Plain { 239 | o.newLive() 240 | } 241 | } 242 | } 243 | 244 | // ioFd is an interface that is implemented by things that have a file 245 | // descriptor. We use this to check if the io.Writer is a TTY. 246 | type ioFd interface { 247 | Fd() uintptr 248 | } 249 | 250 | const ( 251 | iconNormal = "" 252 | iconWarning = "⚠️ " 253 | iconError = "🚫" 254 | iconSuccess = "✅" 255 | iconSpace = " " 256 | ) 257 | -------------------------------------------------------------------------------- /output_xlsx.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | "strconv" 7 | "sync" 8 | 9 | "github.com/360EntSecGroup-Skylar/excelize" 10 | "github.com/mitchellh/golicense/config" 11 | "github.com/mitchellh/golicense/license" 12 | "github.com/mitchellh/golicense/module" 13 | ) 14 | 15 | // XLSXOutput writes the results of license lookups to an XLSX file. 16 | type XLSXOutput struct { 17 | // Path is the path to the file to write. This will be overwritten if 18 | // it exists. 19 | Path string 20 | 21 | // Config is the configuration (if any). This will be used to check 22 | // if a license is allowed or not. 23 | Config *config.Config 24 | 25 | modules map[*module.Module]interface{} 26 | lock sync.Mutex 27 | } 28 | 29 | // Start implements Output 30 | func (o *XLSXOutput) Start(m *module.Module) {} 31 | 32 | // Update implements Output 33 | func (o *XLSXOutput) Update(m *module.Module, t license.StatusType, msg string) {} 34 | 35 | // Finish implements Output 36 | func (o *XLSXOutput) Finish(m *module.Module, l *license.License, err error) { 37 | o.lock.Lock() 38 | defer o.lock.Unlock() 39 | 40 | if o.modules == nil { 41 | o.modules = make(map[*module.Module]interface{}) 42 | } 43 | 44 | o.modules[m] = l 45 | if err != nil { 46 | o.modules[m] = err 47 | } 48 | } 49 | 50 | // Close implements Output 51 | func (o *XLSXOutput) Close() error { 52 | o.lock.Lock() 53 | defer o.lock.Unlock() 54 | 55 | const s = "Sheet1" 56 | f := excelize.NewFile() 57 | 58 | // Headers 59 | f.SetCellValue(s, "A1", "Dependency") 60 | f.SetCellValue(s, "B1", "Version") 61 | f.SetCellValue(s, "C1", "SPDX ID") 62 | f.SetCellValue(s, "D1", "License") 63 | f.SetCellValue(s, "E1", "Allowed") 64 | f.SetColWidth(s, "A", "A", 40) 65 | f.SetColWidth(s, "B", "B", 20) 66 | f.SetColWidth(s, "C", "C", 20) 67 | f.SetColWidth(s, "D", "D", 40) 68 | f.SetColWidth(s, "E", "E", 10) 69 | 70 | // Create all our styles 71 | redStyle, _ := f.NewStyle(`{"fill":{"type":"pattern","pattern":1,"color":["#FFCCCC"]}}`) 72 | yellowStyle, _ := f.NewStyle(`{"fill":{"type":"pattern","pattern":1,"color":["#FFC107"]}}`) 73 | greenStyle, _ := f.NewStyle(`{"fill":{"type":"pattern","pattern":1,"color":["#9CCC65"]}}`) 74 | 75 | // Sort the modules by name 76 | keys := make([]string, 0, len(o.modules)) 77 | index := map[string]*module.Module{} 78 | for m := range o.modules { 79 | keys = append(keys, m.Path) 80 | index[m.Path] = m 81 | } 82 | sort.Strings(keys) 83 | 84 | // Go through each module and output it into the spreadsheet 85 | for i, k := range keys { 86 | row := strconv.FormatInt(int64(i+2), 10) 87 | 88 | m := index[k] 89 | f.SetCellValue(s, "A"+row, m.Path) 90 | f.SetCellValue(s, "B"+row, m.Version) 91 | f.SetCellValue(s, "E"+row, "unknown") 92 | f.SetCellStyle(s, "A"+row, "A"+row, yellowStyle) 93 | f.SetCellStyle(s, "B"+row, "B"+row, yellowStyle) 94 | f.SetCellStyle(s, "C"+row, "C"+row, yellowStyle) 95 | f.SetCellStyle(s, "D"+row, "D"+row, yellowStyle) 96 | f.SetCellStyle(s, "E"+row, "E"+row, yellowStyle) 97 | 98 | raw := o.modules[m] 99 | if raw == nil { 100 | f.SetCellValue(s, "D"+row, "no") 101 | f.SetCellStyle(s, "A"+row, "A"+row, redStyle) 102 | f.SetCellStyle(s, "B"+row, "B"+row, redStyle) 103 | f.SetCellStyle(s, "C"+row, "C"+row, redStyle) 104 | f.SetCellStyle(s, "D"+row, "D"+row, redStyle) 105 | f.SetCellStyle(s, "E"+row, "E"+row, redStyle) 106 | continue 107 | } 108 | 109 | // If the value is an error, then note the error 110 | if err, ok := raw.(error); ok { 111 | f.SetCellValue(s, "D"+row, fmt.Sprintf("ERROR: %s", err)) 112 | f.SetCellValue(s, "E"+row, "no") 113 | f.SetCellStyle(s, "A"+row, "A"+row, redStyle) 114 | f.SetCellStyle(s, "B"+row, "B"+row, redStyle) 115 | f.SetCellStyle(s, "C"+row, "C"+row, redStyle) 116 | f.SetCellStyle(s, "D"+row, "D"+row, redStyle) 117 | f.SetCellStyle(s, "E"+row, "E"+row, redStyle) 118 | continue 119 | } 120 | 121 | // If the value is a license, then mark the license 122 | if lic, ok := raw.(*license.License); ok { 123 | if lic != nil { 124 | f.SetCellValue(s, fmt.Sprintf("C%d", i+2), lic.SPDX) 125 | } 126 | f.SetCellValue(s, fmt.Sprintf("D%d", i+2), lic.String()) 127 | if o.Config != nil { 128 | switch o.Config.Allowed(lic) { 129 | case config.StateAllowed: 130 | f.SetCellValue(s, fmt.Sprintf("E%d", i+2), "yes") 131 | f.SetCellStyle(s, "A"+row, "A"+row, greenStyle) 132 | f.SetCellStyle(s, "B"+row, "B"+row, greenStyle) 133 | f.SetCellStyle(s, "C"+row, "C"+row, greenStyle) 134 | f.SetCellStyle(s, "D"+row, "D"+row, greenStyle) 135 | f.SetCellStyle(s, "E"+row, "E"+row, greenStyle) 136 | 137 | case config.StateDenied: 138 | f.SetCellValue(s, fmt.Sprintf("E%d", i+2), "no") 139 | f.SetCellStyle(s, "A"+row, "A"+row, redStyle) 140 | f.SetCellStyle(s, "B"+row, "B"+row, redStyle) 141 | f.SetCellStyle(s, "C"+row, "C"+row, redStyle) 142 | f.SetCellStyle(s, "D"+row, "D"+row, redStyle) 143 | f.SetCellStyle(s, "E"+row, "E"+row, redStyle) 144 | } 145 | } 146 | } 147 | } 148 | 149 | // Save 150 | if err := f.SaveAs(o.Path); err != nil { 151 | return err 152 | } 153 | 154 | return nil 155 | } 156 | -------------------------------------------------------------------------------- /semaphore.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // Semaphore is a this wrapper around a channel for using it as a semaphore. 4 | type Semaphore chan struct{} 5 | 6 | // NewSemaphore creates a semaphore that allows up to a given limit of 7 | // simultaneous acquisitions 8 | func NewSemaphore(n int) Semaphore { 9 | if n == 0 { 10 | panic("semaphore with limit 0") 11 | } 12 | 13 | ch := make(chan struct{}, n) 14 | return Semaphore(ch) 15 | } 16 | 17 | // Acquire is used to acquire an available slot. Blocks until available. 18 | func (s Semaphore) Acquire() { 19 | s <- struct{}{} 20 | } 21 | 22 | // Release is used to return a slot. Acquire must be called as a pre-condition. 23 | func (s Semaphore) Release() { 24 | select { 25 | case <-s: 26 | default: 27 | panic("release without an acquire") 28 | } 29 | } 30 | --------------------------------------------------------------------------------