├── .gitignore ├── .travis.yml ├── GNUmakefile ├── LICENSE ├── README.md ├── common ├── logical_value.go ├── logical_value_test.go ├── utilities.go ├── utilities_test.go ├── well_formed_name.go └── well_formed_name_test.go ├── coverage.sh ├── examples └── main.go ├── go.mod ├── go.sum ├── matching ├── cpe_name_matcher.go └── cpe_name_matcher_test.go ├── naming ├── cpe_name_binder.go ├── cpe_name_binder_test.go ├── cpe_name_unbinder.go └── cpe_name_unbinder_test.go └── testing ├── README.md ├── dictionary_test.go ├── dictionary_test.tmpl └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | 7 | # Test binary, build with `go test -c` 8 | *.test 9 | 10 | # Output of the go coverage tool, specifically when used with LiteIDE 11 | *.out 12 | 13 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 14 | .glide/ 15 | 16 | vendor/ 17 | 18 | coverage.html 19 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | arch: 3 | - amd64 4 | - ppc64le 5 | 6 | sudo: false 7 | go: 8 | - 1.15 9 | before_install: 10 | - go get github.com/mattn/goveralls 11 | script: 12 | - make test 13 | - make cov 14 | - $GOPATH/bin/goveralls -service=travis-ci -coverprofile=$TRAVIS_BUILD_DIR/coverage.out 15 | env: 16 | global: 17 | secure: emU3C7MweW6/xLKq1nLCphDa0XDWcK/ajuhyJ/s00NoMhbCtsiPypmVL8edRSP9GrYvHIyYMX8wv45d0cHe1Wjp9SzNE6oJNkTuNQFtfxJMcpymp77Cb/CHXkv95AlxvubQQe+0NdK0C4YMsv/GoBfj9qGD6MQOdaOFDLl8UNLW6czTZ+BHpAg7PhSPQLI6ftznmAA7sfPdYNOKdEIdYe4NEcH3/rfseCuEDpMaP28Qfue2uZ0mmfjEmj11wZM/yBgWxl2ZnhJLwXlW6tgeh7O0EQBIO99uNbZ57SWrQhABaTwqhZ9Yh/90R3Z5RsnWucJu8wL4b2fowI8V5+ORzuuNj0zV160JjAXa/Y6nmzz+c6P9DQrrS/wQ7RZGCBZoroCtkz9NKPA7xCAh6QY6QTx1rx1Gs3iyfnrvQk40jbJSORc0b3R6/TzteMTaV5Gu9NDCuU1j1BnDxsrhqh6M/y5mbkwpCk1/+rUFTeK/+crZxHhE6Y6+6yxvBYsl3+Jg41NB5n99+CsQA1RrxTIoFdO6K9SJeP2l4f9HNi1sBaz3SFlD/9uaTGjorj7+UYn8m04wxnEthPyevTzvTBGkkA/iVOW6aGmgst1kyODiom4CQfCt30jM6aroKbU3Z7vL9uNeUmCfSDNLyJ+AgfQPA0q5JsKRD9FyVBx0Bk9Mr0Sg= 18 | -------------------------------------------------------------------------------- /GNUmakefile: -------------------------------------------------------------------------------- 1 | .PHONY: \ 2 | all \ 3 | update \ 4 | build \ 5 | install \ 6 | lint \ 7 | vet \ 8 | fmt \ 9 | fmtcheck \ 10 | clean \ 11 | pretest \ 12 | test 13 | 14 | SRCS = $(shell git ls-files '*.go') 15 | PKGS = $(shell go list ./... | grep -v /vendor/) 16 | COVERAGE_PKGS = $(shell go list ./... | grep -Ev "/testing|/examples") 17 | 18 | all: build test 19 | 20 | build: main.go 21 | go build -o go-cpe $< 22 | 23 | 24 | lint: 25 | @ go get -v github.com/golang/lint/golint 26 | $(foreach file,$(SRCS),golint $(file) || exit;) 27 | 28 | vet: 29 | go vet $(PKGS) || exit; 30 | 31 | fmt: 32 | gofmt -w $(SRCS) 33 | 34 | fmtcheck: 35 | @ $(foreach file,$(SRCS),gofmt -s -l $(file);) 36 | cov: 37 | sh coverage.sh $(COVERAGE_PKGS) || exit; 38 | go tool cover -html=coverage.out -o coverage.html 39 | 40 | clean: 41 | go clean $(shell glide nv) 42 | 43 | pretest: vet fmtcheck 44 | 45 | test: pretest 46 | @ $(foreach pkg,$(PKGS), go test $(pkg) || exit;) 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Teppei Fukuda 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 | # go-cpe 2 | 3 | [![Build Status](https://travis-ci.org/knqyf263/go-cpe.svg?branch=master)](https://travis-ci.org/knqyf263/go-cpe) 4 | [![Coverage Status](https://coveralls.io/repos/github/knqyf263/go-cpe/badge.svg?branch=initial)](https://coveralls.io/github/knqyf263/go-cpe?branch=initial) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/knqyf263/go-cpe)](https://goreportcard.com/report/github.com/knqyf263/go-cpe) 6 | [![GoDoc](https://godoc.org/github.com/knqyf263/go-cpe?status.svg)](https://godoc.org/github.com/knqyf263/go-cpe) 7 | [![MIT License](http://img.shields.io/badge/license-MIT-blue.svg?style=flat)](/LICENSE) 8 | 9 | A Go library for [CPE(A Common Platform Enumeration 2.3)](https://cpe.mitre.org/specification/) 10 | 11 | `go-cpe` is a implementation of the CPE Naming and Matching algorithms, as described in NIST IRs [7695](https://csrc.nist.gov/publications/detail/nistir/7695/final) and [7696](https://csrc.nist.gov/publications/detail/nistir/7696/final). 12 | 13 | For the reference Java implementation, see: https://cpe.mitre.org/specification/CPE_Reference_Implementation_Sep2011.zipx 14 | 15 | # Installation and Usage 16 | 17 | Installation can be done with a normal go get: 18 | 19 | ``` 20 | $ go get github.com/knqyf263/go-cpe/... 21 | ``` 22 | 23 | ## Compare 24 | See [example](/examples) 25 | 26 | ``` 27 | package main 28 | 29 | import ( 30 | "fmt" 31 | 32 | "github.com/knqyf263/go-cpe/matching" 33 | "github.com/knqyf263/go-cpe/naming" 34 | ) 35 | 36 | func main() { 37 | wfn, err := naming.UnbindURI("cpe:/a:microsoft:internet_explorer%01%01%01%01:8%02:beta") 38 | if err != nil { 39 | fmt.Println(err) 40 | return 41 | } 42 | wfn2, err := naming.UnbindFS(`cpe:2.3:a:microsoft:internet_explorer:8.0.6001:beta:*:*:*:*:*:*`) 43 | if err != nil { 44 | fmt.Println(err) 45 | return 46 | } 47 | fmt.Println(matching.IsDisjoint(wfn, wfn2)) 48 | fmt.Println(matching.IsEqual(wfn, wfn2)) 49 | fmt.Println(matching.IsSubset(wfn, wfn2)) 50 | fmt.Println(matching.IsSuperset(wfn, wfn2)) 51 | } 52 | ``` 53 | 54 | # Contribute 55 | 56 | 1. fork a repository: github.com/knqyf263/go-cpe to github.com/you/repo 57 | 2. get original code: `go get github.com/knqyf263/go-cpe` 58 | 3. work on original code 59 | 4. add remote to your repo: git remote add myfork https://github.com/you/repo.git 60 | 5. push your changes: git push myfork 61 | 6. create a new Pull Request 62 | 63 | - see [GitHub and Go: forking, pull requests, and go-getting](http://blog.campoy.cat/2014/03/github-and-go-forking-pull-requests-and.html) 64 | 65 | ---- 66 | 67 | # License 68 | MIT 69 | 70 | # Author 71 | Teppei Fukuda 72 | -------------------------------------------------------------------------------- /common/logical_value.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | ) 7 | 8 | var ( 9 | // ErrIllegalArgument is returned when illegal argument 10 | ErrIllegalArgument = errors.New("Illegal argument") 11 | ) 12 | 13 | // LogicalValue represents a Logical Value. 14 | // @see cpe.mitre.org for more information. 15 | // @author JKRAUNELIS 16 | // @email jkraunelis@mitre.org 17 | type LogicalValue struct { 18 | Any bool 19 | Na bool 20 | } 21 | 22 | // NewLogicalValue returns Logicalvalue 23 | func NewLogicalValue(t string) (lv LogicalValue, err error) { 24 | t = strings.ToUpper(t) 25 | if t == "ANY" { 26 | lv.Any = true 27 | } else if t == "NA" { 28 | lv.Na = true 29 | } else { 30 | return LogicalValue{}, ErrIllegalArgument 31 | } 32 | return lv, nil 33 | } 34 | 35 | // IsANY returns whether any is true 36 | func (lv LogicalValue) IsANY() bool { 37 | return lv.Any 38 | } 39 | 40 | // IsNA returns whether na is true 41 | func (lv LogicalValue) IsNA() bool { 42 | return lv.Na 43 | } 44 | 45 | // String : String 46 | func (lv LogicalValue) String() string { 47 | if lv.Any { 48 | return "ANY" 49 | } 50 | return "NA" 51 | } 52 | -------------------------------------------------------------------------------- /common/logical_value_test.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/pkg/errors" 7 | ) 8 | 9 | func TestNewLogicalValue(t *testing.T) { 10 | vectors := []struct { 11 | s string 12 | expected LogicalValue 13 | wantErr error 14 | }{{ 15 | s: `ANY`, 16 | expected: LogicalValue{Any: true}, 17 | }, { 18 | s: `NA`, 19 | expected: LogicalValue{Na: true}, 20 | }, { 21 | s: `any`, 22 | expected: LogicalValue{Any: true}, 23 | }, { 24 | s: `na`, 25 | expected: LogicalValue{Na: true}, 26 | }, { 27 | s: `invalid`, 28 | wantErr: ErrIllegalArgument, 29 | }, 30 | } 31 | 32 | for i, v := range vectors { 33 | actual, err := NewLogicalValue(v.s) 34 | if errors.Cause(err) != v.wantErr { 35 | t.Errorf("test %d, Error: got %v, want %v", i, errors.Cause(err), v.wantErr) 36 | } 37 | if err != nil { 38 | continue 39 | } 40 | if actual != v.expected { 41 | t.Errorf("test %d, Result: got %v, want %v", i, actual, v.expected) 42 | } 43 | } 44 | } 45 | 46 | func TestIsANY(t *testing.T) { 47 | vectors := []struct { 48 | lv LogicalValue 49 | expected bool 50 | }{{ 51 | lv: LogicalValue{Any: true, Na: false}, 52 | expected: true, 53 | }, { 54 | lv: LogicalValue{Any: false, Na: true}, 55 | expected: false, 56 | }, 57 | } 58 | 59 | for i, v := range vectors { 60 | actual := v.lv.IsANY() 61 | if actual != v.expected { 62 | t.Errorf("test %d, Result: got %v, want %v", i, actual, v.expected) 63 | } 64 | } 65 | } 66 | 67 | func TestIsNA(t *testing.T) { 68 | vectors := []struct { 69 | lv LogicalValue 70 | expected bool 71 | }{{ 72 | lv: LogicalValue{Any: true, Na: false}, 73 | expected: false, 74 | }, { 75 | lv: LogicalValue{Any: false, Na: true}, 76 | expected: true, 77 | }, 78 | } 79 | 80 | for i, v := range vectors { 81 | actual := v.lv.IsNA() 82 | if actual != v.expected { 83 | t.Errorf("test %d, Result: got %v, want %v", i, actual, v.expected) 84 | } 85 | } 86 | } 87 | 88 | func TestLogicalValueString(t *testing.T) { 89 | vectors := []struct { 90 | lv LogicalValue 91 | expected string 92 | }{{ 93 | lv: LogicalValue{Any: true, Na: false}, 94 | expected: "ANY", 95 | }, { 96 | lv: LogicalValue{Any: false, Na: true}, 97 | expected: "NA", 98 | }, 99 | } 100 | 101 | for i, v := range vectors { 102 | actual := v.lv.String() 103 | if actual != v.expected { 104 | t.Errorf("test %d, Result: got %v, want %v", i, actual, v.expected) 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /common/utilities.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "strings" 5 | "unicode" 6 | 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | // IndexOf searches a string for the first occurrence of another string, starting 11 | // at a given offset. 12 | // @param str1 String to search. 13 | // @param str2 String to search for. 14 | // @param off Integer offset or -1 if not found. 15 | func IndexOf(str1, str2 string, off int) int { 16 | index := strings.Index(str1[off:], str2) 17 | if index == -1 { 18 | return -1 19 | } 20 | return index + off 21 | } 22 | 23 | // ContainsWildcards searches string for special characters * and ? 24 | // @param string String to be searched 25 | // @return true if string contains wildcard, false otherwise 26 | func ContainsWildcards(str string) bool { 27 | prev := ' ' 28 | for _, s := range str { 29 | if s == '*' || s == '?' { 30 | if prev != '\\' { 31 | return true 32 | } 33 | } 34 | prev = s 35 | } 36 | return false 37 | } 38 | 39 | // ContainsQuestions searches string for special characters ? 40 | // @param string String to be searched 41 | // @return true if string contains wildcard, false otherwise 42 | func ContainsQuestions(str string) bool { 43 | prev := ' ' 44 | for _, s := range str { 45 | if s == '?' { 46 | if prev != '\\' { 47 | return true 48 | } 49 | } 50 | prev = s 51 | } 52 | return false 53 | } 54 | 55 | // CountEscapeCharacters counts the number of escape characters in the string beginning and ending 56 | // at the given indices 57 | // @param str string to search 58 | // @param start beginning index 59 | // @param end ending index 60 | // @return number of escape characters in string 61 | func CountEscapeCharacters(str string) (result int) { 62 | active := false 63 | for _, s := range str { 64 | if !active && s == '\\' { 65 | result++ 66 | active = true 67 | } else { 68 | active = false 69 | } 70 | } 71 | return result 72 | } 73 | 74 | // LengthWithEscapeCharacters counts the number of characters with escape characters 75 | func LengthWithEscapeCharacters(str string) (result int) { 76 | return len(str) - CountEscapeCharacters(str) 77 | } 78 | 79 | // GetUnescapedColonIndex searches a string for the first unescaped colon and returns the index of that colon 80 | // @param str string to search 81 | // @return index of first unescaped colon, or 0 if not found 82 | func GetUnescapedColonIndex(str string) (idx int) { 83 | for i, s := range str { 84 | if s == ':' { 85 | if i == 0 || str[i-1] != '\\' { 86 | idx = i 87 | break 88 | } 89 | } 90 | } 91 | return idx 92 | } 93 | 94 | // IsAlphanum returns true if the string contains only 95 | // alphanumeric characters or the underscore character, 96 | // false otherwise. 97 | // @param c the string in question 98 | // @return true if c is alphanumeric or underscore, false if not 99 | func IsAlphanum(s string) bool { 100 | for _, r := range s { 101 | if !IsAlpha(r) && !unicode.IsDigit(r) && r != '_' { 102 | return false 103 | } 104 | } 105 | return true 106 | } 107 | 108 | // IsAlpha returns true if the rune contains only 'a'..'z' and 'A'..'Z'. 109 | func IsAlpha(r rune) bool { 110 | if (r < 'a' || r > 'z') && (r < 'A' || r > 'Z') { 111 | return false 112 | } 113 | return true 114 | } 115 | 116 | // ValidateURI is not part of the reference implementation pseudo code 117 | // found in the CPE 2.3 specification. It enforces two rules in the 118 | // specification: 119 | // URI must start with the characters "cpe:/" 120 | // A URI may not contain more than 7 components 121 | // If either rule is violated, a ParseErr is thrown. 122 | func ValidateURI(in string) error { 123 | in = strings.ToLower(in) 124 | // make sure uri starts with cpe:/ 125 | if !strings.HasPrefix(in, "cpe:/") { 126 | return errors.Wrapf(ErrParse, "Error: URI must start with 'cpe:/'. Given: %s", in) 127 | } 128 | // make sure uri doesn't contain more than 7 colons 129 | if count := strings.Count(in, ":"); count > 7 { 130 | return errors.Wrapf(ErrParse, "Error parsing URI. Found %d extra components in: %s", count-7, in) 131 | } 132 | return nil 133 | } 134 | 135 | // ValidateFS is not part of the reference implementation pseudo code 136 | // found in the CPE 2.3 specification. It enforces three rules found in the 137 | // specification: 138 | // Formatted string must start with the characters "cpe:2.3:" 139 | // A formatted string must contain 11 components 140 | // A formatted string must not contain empty components 141 | // If any rule is violated, a ParseException is thrown. 142 | func ValidateFS(in string) error { 143 | in = strings.ToLower(in) 144 | if !strings.HasPrefix(in, "cpe:2.3:") { 145 | return errors.Wrapf(ErrParse, "Error: Formatted String must start with \"cpe:2.3\". Given: %s", in) 146 | } 147 | // make sure fs contains exactly 12 unquoted colons 148 | count := 0 149 | for i := 0; i != len(in); i++ { 150 | if in[i] == ':' { 151 | if i == 0 || in[i-1] != '\\' { 152 | count++ 153 | } else { 154 | continue 155 | } 156 | if i < len(in)-1 && in[i+1] == ':' { 157 | return errors.Wrap(ErrParse, "Error parsing formatted string. Found empty component") 158 | } 159 | } 160 | } 161 | if count > 12 { 162 | extra := count - 12 163 | return errors.Wrapf(ErrParse, "Error parsing formatted string. Found %d components in: %s", extra, in) 164 | } 165 | if count < 12 { 166 | missing := 12 - count 167 | return errors.Wrapf(ErrParse, "Error parsing formatted string. Missing %d components in: %s", missing, in) 168 | } 169 | return nil 170 | } 171 | -------------------------------------------------------------------------------- /common/utilities_test.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/pkg/errors" 7 | ) 8 | 9 | func TestCountEscapeCharacters(t *testing.T) { 10 | vectors := []struct { 11 | s string 12 | expected int 13 | }{{ 14 | s: `\abc`, 15 | expected: 1, 16 | }, { 17 | s: `\\abc`, 18 | expected: 1, 19 | }, { 20 | s: `\\\abc`, 21 | expected: 2, 22 | }, { 23 | s: `\\\\abc`, 24 | expected: 2, 25 | }, { 26 | s: `\\abc\d`, 27 | expected: 2, 28 | }, { 29 | s: `\\abc\\d`, 30 | expected: 2, 31 | }, { 32 | s: `\\abc\\\d`, 33 | expected: 3, 34 | }, 35 | } 36 | 37 | for i, v := range vectors { 38 | actual := CountEscapeCharacters(v.s) 39 | if actual != v.expected { 40 | t.Errorf("test %d, Result: got %v, want %v", i, actual, v.expected) 41 | } 42 | } 43 | } 44 | 45 | func TestLengthWithEscapeCharacters(t *testing.T) { 46 | vectors := []struct { 47 | s string 48 | expected int 49 | }{{ 50 | s: `\abc`, 51 | expected: 3, 52 | }, { 53 | s: `\\abc`, 54 | expected: 4, 55 | }, { 56 | s: `\\\abc`, 57 | expected: 4, 58 | }, { 59 | s: `\\\\abc`, 60 | expected: 5, 61 | }, { 62 | s: `\\abc\d`, 63 | expected: 5, 64 | }, { 65 | s: `\\abc\\d`, 66 | expected: 6, 67 | }, { 68 | s: `\\abc\\\d`, 69 | expected: 6, 70 | }, 71 | } 72 | 73 | for i, v := range vectors { 74 | actual := LengthWithEscapeCharacters(v.s) 75 | if actual != v.expected { 76 | t.Errorf("test %d, Result: got %v, want %v", i, actual, v.expected) 77 | } 78 | } 79 | } 80 | 81 | func TestContainsWildCards(t *testing.T) { 82 | vectors := []struct { 83 | s string 84 | expected bool 85 | }{{ 86 | s: "*abc", 87 | expected: true, 88 | }, { 89 | s: "?abc", 90 | expected: true, 91 | }, { 92 | s: "abc*", 93 | expected: true, 94 | }, { 95 | s: `abc\*`, 96 | expected: false, 97 | }, { 98 | s: "abc??", 99 | expected: true, 100 | }, { 101 | s: "abc*def", 102 | expected: true, 103 | }, { 104 | s: "abc?def", 105 | expected: true, 106 | }, { 107 | s: `abc\:def*`, 108 | expected: true, 109 | }, { 110 | s: `abc\:def:ghi`, 111 | expected: false, 112 | }, { 113 | s: `abc\:def:ghi*`, 114 | expected: true, 115 | }, { 116 | s: `abc\*def`, 117 | expected: false, 118 | }, { 119 | s: `abc\*def*`, 120 | expected: true, 121 | }, 122 | } 123 | 124 | for i, v := range vectors { 125 | actual := ContainsWildcards(v.s) 126 | if actual != v.expected { 127 | t.Errorf("test %d, Result: got %v, want %v", i, actual, v.expected) 128 | } 129 | } 130 | } 131 | 132 | func TestContainsQuestions(t *testing.T) { 133 | vectors := []struct { 134 | s string 135 | expected bool 136 | }{{ 137 | s: "*abc", 138 | expected: false, 139 | }, { 140 | s: "?abc", 141 | expected: true, 142 | }, { 143 | s: "abc*", 144 | expected: false, 145 | }, { 146 | s: `abc\*`, 147 | expected: false, 148 | }, { 149 | s: "abc??", 150 | expected: true, 151 | }, { 152 | s: "abc*def", 153 | expected: false, 154 | }, { 155 | s: "abc?def", 156 | expected: true, 157 | }, { 158 | s: `abc\:def*`, 159 | expected: false, 160 | }, { 161 | s: `abc\:def:ghi`, 162 | expected: false, 163 | }, { 164 | s: `abc\:def:ghi*`, 165 | expected: false, 166 | }, { 167 | s: `abc\?def`, 168 | expected: false, 169 | }, { 170 | s: `abc\?def?`, 171 | expected: true, 172 | }, 173 | } 174 | 175 | for i, v := range vectors { 176 | actual := ContainsQuestions(v.s) 177 | if actual != v.expected { 178 | t.Errorf("test %d, Result: got %v, want %v", i, actual, v.expected) 179 | } 180 | } 181 | } 182 | 183 | func TestGetUnescapedColonIndex(t *testing.T) { 184 | vectors := []struct { 185 | s string 186 | expected int 187 | }{{ 188 | s: "abc", 189 | expected: 0, 190 | }, { 191 | s: ":abc", 192 | expected: 0, 193 | }, { 194 | s: "abc:", 195 | expected: 3, 196 | }, { 197 | s: `abc\:def`, 198 | expected: 0, 199 | }, { 200 | s: `abc\:def:ghi`, 201 | expected: 8, 202 | }, 203 | } 204 | 205 | for i, v := range vectors { 206 | actual := GetUnescapedColonIndex(v.s) 207 | if actual != v.expected { 208 | t.Errorf("test %d, Result: got %v, want %v", i, actual, v.expected) 209 | } 210 | } 211 | } 212 | 213 | func TestIsAlphanum(t *testing.T) { 214 | vectors := []struct { 215 | s string 216 | expected bool 217 | }{{ 218 | s: "abc", 219 | expected: true, 220 | }, { 221 | s: "ABC123", 222 | expected: true, 223 | }, { 224 | s: "xyz_789", 225 | expected: true, 226 | }, { 227 | s: "abc XYZ", 228 | expected: false, 229 | }, { 230 | s: "def!456", 231 | expected: false, 232 | }, { 233 | s: "abcあABC", 234 | expected: false, 235 | }, 236 | } 237 | 238 | for i, v := range vectors { 239 | actual := IsAlphanum(v.s) 240 | if actual != v.expected { 241 | t.Errorf("test %d, Result: got %v, want %v", i, actual, v.expected) 242 | } 243 | } 244 | } 245 | 246 | func TestIsAlpha(t *testing.T) { 247 | vectors := []struct { 248 | r rune 249 | expected bool 250 | }{{ 251 | r: 'a', 252 | expected: true, 253 | }, { 254 | r: 'A', 255 | expected: true, 256 | }, { 257 | r: '!', 258 | expected: false, 259 | }, { 260 | r: ' ', 261 | expected: false, 262 | }, { 263 | r: 'あ', 264 | expected: false, 265 | }, {}, 266 | } 267 | 268 | for i, v := range vectors { 269 | actual := IsAlpha(v.r) 270 | if actual != v.expected { 271 | t.Errorf("test %d, Result: got %v, want %v", i, actual, v.expected) 272 | } 273 | } 274 | } 275 | 276 | func TestValidateURI(t *testing.T) { 277 | vectors := []struct { 278 | s string 279 | wantErr error 280 | }{{ 281 | s: `cpe:/a:microsoft:internet_explorer:8.0.6001:beta::sp2`, 282 | }, { 283 | s: `cpe:/a:microsoft:internet_explorer:8.0.6001:beta::sp2:invalid`, 284 | wantErr: ErrParse, 285 | }, { 286 | s: `cpe:2.3:a:microsoft:internet_explorer:8.0.6001:beta:*:sp2:*:*:*:*`, 287 | wantErr: ErrParse, 288 | }, 289 | } 290 | 291 | for i, v := range vectors { 292 | err := ValidateURI(v.s) 293 | if errors.Cause(err) != v.wantErr { 294 | t.Errorf("test %d, Error: got %v, want %v", i, err, v.wantErr) 295 | } 296 | } 297 | } 298 | 299 | func TestValidateFS(t *testing.T) { 300 | vectors := []struct { 301 | s string 302 | wantErr error 303 | }{{ 304 | s: `cpe:2.3:a:microsoft:internet_explorer:8.0.6001:beta:*:sp2:*:*:*:*`, 305 | }, { 306 | s: `cpe:2.3:a:microsoft:internet_explorer:8.0.6001:beta:*:sp2:*:*\::*:*`, 307 | }, { 308 | s: `cpe:2.3:a:microsoft:internet_explorer:8.0.6001:beta:*:sp2:*:*\:\::*:*`, 309 | }, { 310 | s: "cpe:2.3:a:microsoft:internet_explorer:8.0.6001:beta:*:sp2:*:*:*:*:*", 311 | wantErr: ErrParse, 312 | }, { 313 | s: "cpe:2.3:a:microsoft:internet_explorer:8.0.6001:beta:*:sp2:*:*::*", 314 | wantErr: ErrParse, 315 | }, { 316 | s: "cpe:2.4:a:microsoft:internet_explorer:8.0.6001:beta:*:sp2:*:*:*:*", 317 | wantErr: ErrParse, 318 | }, { 319 | s: "cpe:2.3:a:microsoft:internet_explorer:8.0.6001:beta:*:sp2:", 320 | wantErr: ErrParse, 321 | }, 322 | } 323 | 324 | for i, v := range vectors { 325 | err := ValidateFS(v.s) 326 | if errors.Cause(err) != v.wantErr { 327 | t.Errorf("test %d, Error: got %v, want %v", i, err, v.wantErr) 328 | } 329 | } 330 | } 331 | -------------------------------------------------------------------------------- /common/well_formed_name.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "unicode" 7 | 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | const ( 12 | AttributePart = "part" 13 | AttributeVendor = "vendor" 14 | AttributeProduct = "product" 15 | AttributeVersion = "version" 16 | AttributeUpdate = "update" 17 | AttributeEdition = "edition" 18 | AttributeLanguage = "language" 19 | AttributeSwEdition = "sw_edition" 20 | AttributeTargetSw = "target_sw" 21 | AttributeTargetHw = "target_hw" 22 | AttributeOther = "other" 23 | ) 24 | 25 | var ( 26 | attributes = []string{AttributePart, AttributeVendor, AttributeProduct, AttributeVersion, AttributeUpdate, 27 | AttributeEdition, AttributeLanguage, AttributeSwEdition, AttributeTargetSw, AttributeTargetHw, AttributeOther} 28 | 29 | // ErrIllegalAttribute is returned when illegal argument 30 | ErrIllegalAttribute = errors.New("Illegal attribute") 31 | // ErrParse is returned when parse error 32 | ErrParse = errors.New("Parse error") 33 | ) 34 | 35 | // WellFormedName represents a Well Formed Name, as defined 36 | // in the CPE Specification version 2.3. 37 | // 38 | // @see cpe.mitre.org for details. 39 | type WellFormedName map[string]interface{} 40 | 41 | // NewWellFormedName constructs a new WellFormedName object, with all components set to the default value "ANY". 42 | func NewWellFormedName() WellFormedName { 43 | wfn := WellFormedName{} 44 | for _, a := range attributes { 45 | if a != AttributePart { 46 | wfn[a], _ = NewLogicalValue("ANY") 47 | } 48 | } 49 | return wfn 50 | } 51 | 52 | // Initialize sets each component to the given parameter value. 53 | // If a parameter is null, the component is set to the default value "ANY". 54 | // @param part string representing the part component 55 | // @param vendor string representing the vendor component 56 | // @param product string representing the product component 57 | // @param version string representing the version component 58 | // @param update string representing the update component 59 | // @param edition string representing the edition component 60 | // @param language string representing the language component 61 | // @param sw_edition string representing the sw_edition component 62 | // @param target_sw string representing the target_sw component 63 | // @param target_hw string representing the target_hw component 64 | // @param other string representing the other component 65 | func (wfn WellFormedName) Initialize(part, vendor, product, version, update, edition, language, swEdition, targetSw, targetHw, other interface{}) { 66 | wfn[AttributePart] = part 67 | wfn[AttributeVendor] = vendor 68 | wfn[AttributeProduct] = product 69 | wfn[AttributeVersion] = version 70 | wfn[AttributeUpdate] = update 71 | wfn[AttributeEdition] = edition 72 | wfn[AttributeLanguage] = language 73 | wfn[AttributeSwEdition] = swEdition 74 | wfn[AttributeTargetSw] = targetSw 75 | wfn[AttributeTargetHw] = targetHw 76 | wfn[AttributeOther] = other 77 | } 78 | 79 | // Get gets attribute 80 | // @param attribute String representing the component value to get 81 | // @return the String value of the given component, or default value "ANY" 82 | // if the component does not exist 83 | func (wfn WellFormedName) Get(attribute string) interface{} { 84 | if v, ok := wfn[attribute]; ok { 85 | return v 86 | } 87 | any, _ := NewLogicalValue("ANY") 88 | return any 89 | } 90 | 91 | // Set sets the given attribute to value, if the attribute is in the list of permissible components 92 | // @param attribute String representing the component to set 93 | // @param value Object representing the value of the given component 94 | func (wfn WellFormedName) Set(attribute string, value interface{}) (err error) { 95 | if valid := IsValidAttribute(attribute); !valid { 96 | return ErrIllegalAttribute 97 | } 98 | 99 | if value == nil { 100 | wfn[attribute], _ = NewLogicalValue("ANY") 101 | return nil 102 | } 103 | 104 | if _, ok := value.(LogicalValue); ok { 105 | if attribute == AttributePart { 106 | return errors.Wrap(ErrIllegalAttribute, "part component cannot be a logical value") 107 | } 108 | wfn[attribute] = value 109 | return nil 110 | } 111 | 112 | svalue, ok := value.(string) 113 | if !ok { 114 | return errors.Wrap(ErrIllegalAttribute, "value must be a logical value or string") 115 | } 116 | 117 | if err = ValidateStringValue(svalue); err != nil { 118 | return errors.Wrap(err, "Failed to validate a value") 119 | } 120 | 121 | // part must be a, o, or h 122 | if attribute == AttributePart { 123 | if svalue != "a" && svalue != "o" && svalue != "h" { 124 | return errors.Wrapf(ErrParse, "part component must be one of the following: 'a', 'o', 'h': %s", svalue) 125 | } 126 | } 127 | 128 | // should be good to go 129 | wfn[attribute] = value 130 | 131 | return nil 132 | } 133 | 134 | // GetString gets attribute as string 135 | // @param attribute String representing the component value to get 136 | // @return the String value of the given component, or default value "ANY" 137 | // if the component does not exist 138 | func (wfn WellFormedName) GetString(attribute string) string { 139 | return fmt.Sprintf("%s", wfn.Get(attribute)) 140 | } 141 | 142 | // String returns string representation of the WellFormedName 143 | func (wfn WellFormedName) String() string { 144 | s := "wfn:[" 145 | for _, attr := range attributes { 146 | s += fmt.Sprintf("%s=", attr) 147 | o := wfn.Get(attr) 148 | if lv, ok := o.(LogicalValue); ok { 149 | s += fmt.Sprintf("%s, ", lv) 150 | } else { 151 | s += fmt.Sprintf("\"%s\", ", o) 152 | } 153 | } 154 | s = strings.TrimSpace(s) 155 | s = strings.TrimRight(s, ",") 156 | s += "]" 157 | return s 158 | } 159 | 160 | // IsValidAttribute validates an attribute name 161 | func IsValidAttribute(attribute string) (valid bool) { 162 | for _, a := range attributes { 163 | if a == attribute { 164 | valid = true 165 | } 166 | } 167 | return valid 168 | } 169 | 170 | // ValidateStringValue validates an string value 171 | func ValidateStringValue(svalue string) (err error) { 172 | // svalue has more than one unquoted star 173 | if strings.HasPrefix(svalue, "**") || strings.HasSuffix(svalue, "**") { 174 | return errors.Wrapf(ErrParse, "component cannot contain more than one * in sequence: %s", svalue) 175 | } 176 | 177 | prev := ' ' // dummy value 178 | for i, r := range svalue { 179 | // check for printable characters - no control characters 180 | if !unicode.IsPrint(r) { 181 | return errors.Wrapf(ErrParse, "encountered non printable character in: %s", svalue) 182 | } 183 | // svalue has whitespace 184 | if unicode.IsSpace(r) { 185 | return errors.Wrapf(ErrParse, "component cannot contain whitespace:: %s", svalue) 186 | } 187 | if unicode.IsPunct(r) && prev != '\\' && r != '\\' { 188 | // svalue has an unquoted * 189 | if r == '*' && (i != 0 && i != len(svalue)-1) { 190 | return errors.Wrapf(ErrParse, "component cannot contain embedded *: %s", svalue) 191 | } 192 | 193 | if r != '*' && r != '?' && r != '_' { 194 | // svalue has unquoted punctuation embedded 195 | return errors.Wrapf(ErrParse, "component cannot contain unquoted punctuation: %s", svalue) 196 | } 197 | } 198 | prev = r 199 | } 200 | 201 | if strings.Contains(svalue, "?") { 202 | if svalue == "?" { 203 | // single ? is valid 204 | return nil 205 | } 206 | 207 | s := strings.Trim(svalue, "?") 208 | if ContainsQuestions(s) { 209 | return errors.Wrapf(ErrParse, "component cannot contain embedded ?: %s", svalue) 210 | } 211 | } 212 | 213 | // single asterisk is not allowed 214 | if svalue == "*" { 215 | return errors.Wrapf(ErrParse, "component cannot be a single *: %s", svalue) 216 | } 217 | 218 | // quoted hyphen not allowed by itself 219 | if svalue == `\-` { 220 | return errors.Wrapf(ErrParse, "component cannot be quoted hyphen: %s", svalue) 221 | } 222 | 223 | return nil 224 | } 225 | -------------------------------------------------------------------------------- /common/well_formed_name_test.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | var ( 11 | any, _ = NewLogicalValue("ANY") 12 | na, _ = NewLogicalValue("NA") 13 | ) 14 | 15 | func TestNewWellFormedName(t *testing.T) { 16 | vectors := []struct { 17 | expected WellFormedName 18 | }{{ 19 | expected: WellFormedName{ 20 | "vendor": any, 21 | "product": any, 22 | "version": any, 23 | "update": any, 24 | "edition": any, 25 | "language": any, 26 | "sw_edition": any, 27 | "target_sw": any, 28 | "target_hw": any, 29 | "other": any, 30 | }, 31 | }, 32 | } 33 | 34 | for i, v := range vectors { 35 | actual := NewWellFormedName() 36 | if !reflect.DeepEqual(actual, v.expected) { 37 | t.Errorf("test %d, Result: %v, want %v", i, actual, v.expected) 38 | } 39 | } 40 | } 41 | 42 | func TestWellFormedNameInitialize(t *testing.T) { 43 | vectors := []struct { 44 | part interface{} 45 | vendor interface{} 46 | product interface{} 47 | version interface{} 48 | update interface{} 49 | edition interface{} 50 | language interface{} 51 | swEdition interface{} 52 | targetSw interface{} 53 | targetHw interface{} 54 | other interface{} 55 | expected WellFormedName 56 | }{{ 57 | part: "a", 58 | vendor: "microsoft", 59 | product: "windows_7", 60 | version: na, 61 | update: any, 62 | edition: any, 63 | language: any, 64 | swEdition: any, 65 | targetSw: any, 66 | targetHw: any, 67 | other: any, 68 | expected: WellFormedName{ 69 | "part": "a", 70 | "vendor": "microsoft", 71 | "product": "windows_7", 72 | "version": na, 73 | "update": any, 74 | "edition": any, 75 | "language": any, 76 | "sw_edition": any, 77 | "target_sw": any, 78 | "target_hw": any, 79 | "other": any, 80 | }, 81 | }, 82 | } 83 | 84 | for i, v := range vectors { 85 | wfn := WellFormedName{} 86 | wfn.Initialize(v.part, v.vendor, v.product, v.version, v.update, v.edition, v.language, 87 | v.swEdition, v.targetSw, v.targetHw, v.other) 88 | if !reflect.DeepEqual(wfn, v.expected) { 89 | t.Errorf("test %d, Result: %v, want %v", i, wfn, v.expected) 90 | } 91 | } 92 | } 93 | 94 | func TestWellFormedNameGet(t *testing.T) { 95 | vectors := []struct { 96 | wfn WellFormedName 97 | attribute string 98 | expected interface{} 99 | }{{ 100 | wfn: WellFormedName{ 101 | "part": "a", 102 | }, 103 | attribute: "part", 104 | expected: "a", 105 | }, { 106 | wfn: WellFormedName{ 107 | "vendor": "microsoft", 108 | }, 109 | attribute: "vendor", 110 | expected: "microsoft", 111 | }, { 112 | wfn: WellFormedName{ 113 | "product": any, 114 | }, 115 | attribute: "product", 116 | expected: any, 117 | }, { 118 | wfn: WellFormedName{ 119 | "product": any, 120 | }, 121 | attribute: "version", 122 | expected: any, 123 | }, 124 | } 125 | 126 | for i, v := range vectors { 127 | actual := v.wfn.Get(v.attribute) 128 | if !reflect.DeepEqual(actual, v.expected) { 129 | t.Errorf("test %d, Result: %v, want %v", i, actual, v.expected) 130 | } 131 | } 132 | } 133 | 134 | func TestWellFormedNameGetString(t *testing.T) { 135 | vectors := []struct { 136 | wfn WellFormedName 137 | attribute string 138 | expected interface{} 139 | }{{ 140 | wfn: WellFormedName{ 141 | "part": "a", 142 | }, 143 | attribute: "part", 144 | expected: "a", 145 | }, { 146 | wfn: WellFormedName{ 147 | "vendor": "microsoft", 148 | }, 149 | attribute: "vendor", 150 | expected: "microsoft", 151 | }, { 152 | wfn: WellFormedName{ 153 | "product": any, 154 | }, 155 | attribute: "product", 156 | expected: "ANY", 157 | }, { 158 | wfn: WellFormedName{ 159 | "product": any, 160 | }, 161 | attribute: "version", 162 | expected: "ANY", 163 | }, 164 | } 165 | 166 | for i, v := range vectors { 167 | actual := v.wfn.GetString(v.attribute) 168 | if !reflect.DeepEqual(actual, v.expected) { 169 | t.Errorf("test %d, Result: %v, want %v", i, actual, v.expected) 170 | } 171 | } 172 | } 173 | 174 | func TestWellFormedNameSet(t *testing.T) { 175 | vectors := []struct { 176 | wfn WellFormedName 177 | attribute string 178 | value interface{} 179 | expected WellFormedName 180 | wantErr error 181 | }{{ 182 | wfn: WellFormedName{}, 183 | attribute: "part", 184 | value: "a", 185 | expected: WellFormedName{ 186 | "part": "a", 187 | }, 188 | }, { 189 | wfn: WellFormedName{ 190 | "part": "a", 191 | }, 192 | attribute: "vendor", 193 | value: any, 194 | expected: WellFormedName{ 195 | "part": "a", 196 | "vendor": any, 197 | }, 198 | }, { 199 | wfn: WellFormedName{}, 200 | attribute: "vendor", 201 | value: nil, 202 | expected: WellFormedName{ 203 | "vendor": any, 204 | }, 205 | }, { 206 | wfn: WellFormedName{}, 207 | attribute: "part", 208 | value: na, // part component cannot be a logical value 209 | wantErr: ErrIllegalAttribute, 210 | }, { 211 | wfn: WellFormedName{}, 212 | attribute: "part", 213 | value: "i", // part component must be one of the following: 'a', 'o', 'h' 214 | wantErr: ErrParse, 215 | }, { 216 | wfn: WellFormedName{}, 217 | attribute: "vendor", 218 | value: 1, // value must be a logical value or string 219 | wantErr: ErrIllegalAttribute, 220 | }, { 221 | wfn: WellFormedName{}, 222 | attribute: "invalid", // invalid attribute name 223 | value: "invalid", 224 | wantErr: ErrIllegalAttribute, 225 | }, { 226 | wfn: WellFormedName{}, 227 | attribute: "version", 228 | value: "**1.2.3", 229 | wantErr: ErrParse, 230 | }, 231 | } 232 | 233 | for i, v := range vectors { 234 | err := v.wfn.Set(v.attribute, v.value) 235 | if errors.Cause(err) != v.wantErr { 236 | t.Errorf("test %d, Error: %v, want %v", i, errors.Cause(err), v.wantErr) 237 | } 238 | if err != nil { 239 | continue 240 | } 241 | if !reflect.DeepEqual(v.wfn, v.expected) { 242 | t.Errorf("test %d, Result: %v, want %v", i, v.wfn, v.expected) 243 | } 244 | } 245 | } 246 | 247 | func TestWellFormedNameString(t *testing.T) { 248 | vectors := []struct { 249 | wfn WellFormedName 250 | expected string 251 | }{{ 252 | wfn: WellFormedName{}, 253 | expected: `wfn:[part=ANY, vendor=ANY, product=ANY, version=ANY, update=ANY, edition=ANY, language=ANY, sw_edition=ANY, target_sw=ANY, target_hw=ANY, other=ANY]`, 254 | }, { 255 | wfn: WellFormedName{ 256 | "part": "a", 257 | "vendor": "microsoft", 258 | "product": "windows_7", 259 | "version": na, 260 | "update": any, 261 | "edition": any, 262 | "language": any, 263 | "sw_edition": any, 264 | "target_sw": any, 265 | "target_hw": any, 266 | "other": any, 267 | }, 268 | expected: `wfn:[part="a", vendor="microsoft", product="windows_7", version=NA, update=ANY, edition=ANY, language=ANY, sw_edition=ANY, target_sw=ANY, target_hw=ANY, other=ANY]`, 269 | }, 270 | } 271 | 272 | for i, v := range vectors { 273 | actual := v.wfn.String() 274 | if actual != v.expected { 275 | t.Errorf("test %d, Result: %v, want %v", i, actual, v.expected) 276 | } 277 | } 278 | } 279 | 280 | func TestValidateStringValue(t *testing.T) { 281 | vectors := []struct { 282 | svalue string 283 | wantErr error 284 | }{{ 285 | svalue: "foo", 286 | }, { 287 | svalue: "?", 288 | }, { 289 | svalue: "??", 290 | }, { 291 | svalue: "**foo", 292 | wantErr: ErrParse, 293 | }, { 294 | svalue: "foo**", 295 | wantErr: ErrParse, 296 | }, { 297 | svalue: "foo*bar", 298 | wantErr: ErrParse, 299 | }, { 300 | svalue: "*foo*", 301 | }, { 302 | svalue: "foo!bar", 303 | wantErr: ErrParse, 304 | }, { 305 | svalue: "foo/bar", 306 | wantErr: ErrParse, 307 | }, { 308 | svalue: "foo\x07bar", 309 | wantErr: ErrParse, 310 | }, { 311 | svalue: "foo bar", 312 | wantErr: ErrParse, 313 | }, { 314 | svalue: "???foo??", 315 | }, { 316 | svalue: "??foo?bar??", 317 | wantErr: ErrParse, 318 | }, { 319 | svalue: "*", 320 | wantErr: ErrParse, 321 | }, { 322 | svalue: `\-`, 323 | wantErr: ErrParse, 324 | }, 325 | } 326 | 327 | for i, v := range vectors { 328 | err := ValidateStringValue(v.svalue) 329 | if errors.Cause(err) != v.wantErr { 330 | t.Errorf("test %d, Error: %v, want %v", i, errors.Cause(err), v.wantErr) 331 | } 332 | } 333 | } 334 | -------------------------------------------------------------------------------- /coverage.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | # http://stackoverflow.com/a/21142256/2055281 6 | 7 | echo "mode: atomic" > coverage.out 8 | echo $@ 9 | 10 | for d in $@; do 11 | go test -coverprofile=profile.out -covermode=atomic $d 12 | if [ -f profile.out ]; then 13 | echo "$(pwd)" 14 | cat profile.out | grep -v "mode: " >> coverage.out 15 | rm profile.out 16 | fi 17 | done 18 | -------------------------------------------------------------------------------- /examples/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/knqyf263/go-cpe/common" 7 | "github.com/knqyf263/go-cpe/matching" 8 | "github.com/knqyf263/go-cpe/naming" 9 | ) 10 | 11 | func main() { 12 | wfn, err := naming.UnbindURI("cpe:/a:microsoft:internet_explorer%01%01%01%01:8%02:beta") 13 | if err != nil { 14 | fmt.Println(err) 15 | return 16 | } 17 | wfn2, err := naming.UnbindFS(`cpe:2.3:a:microsoft:internet_explorer:8.0.6001:beta:*:*:*:*:*:*`) 18 | if err != nil { 19 | fmt.Println(err) 20 | return 21 | } 22 | fmt.Println(wfn, wfn2) 23 | fmt.Println(matching.IsDisjoint(wfn, wfn2)) // false 24 | fmt.Println(matching.IsEqual(wfn, wfn2)) // false 25 | fmt.Println(matching.IsSubset(wfn, wfn2)) // false 26 | fmt.Println(matching.IsSuperset(wfn, wfn2)) // true 27 | 28 | wfn3 := common.NewWellFormedName() 29 | wfn3.Set("part", "a") 30 | wfn3.Set("vendor", "microsoft") 31 | wfn3.Set("product", "internet_explorer????") 32 | wfn3.Set("version", "8\\.0\\.6001") 33 | fmt.Println(naming.BindToURI(wfn3)) // cpe:/a:microsoft:internet_explorer%01%01%01%01:8.0.6001 34 | fmt.Println(naming.BindToFS(wfn3)) // cpe:2.3:a:microsoft:internet_explorer????:8.0.6001:*:*:*:*:*:*:* 35 | } 36 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/knqyf263/go-cpe 2 | 3 | go 1.15 4 | 5 | require github.com/pkg/errors v0.8.1 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 2 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 3 | -------------------------------------------------------------------------------- /matching/cpe_name_matcher.go: -------------------------------------------------------------------------------- 1 | package matching 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/knqyf263/go-cpe/common" 7 | ) 8 | 9 | // Relation is enumeration for relational values. 10 | type Relation int 11 | 12 | const ( 13 | // DISJOINT : disjoint 14 | DISJOINT Relation = iota 15 | // SUBSET : subset 16 | SUBSET 17 | // SUPERSET : superset 18 | SUPERSET 19 | // EQUAL : equal 20 | EQUAL 21 | // UNDEFINED : undefined 22 | UNDEFINED 23 | ) 24 | 25 | // IsDisjoint tests two Well Formed Names for disjointness. 26 | // @param source Source WFN 27 | // @param target Target WFN 28 | // @return true if the names are disjoint, false otherwise 29 | func IsDisjoint(source, target common.WellFormedName) bool { 30 | // if any pairwise comparison is disjoint, the names are disjoint. 31 | results := CompareWFNs(source, target) 32 | for _, result := range results { 33 | if result == DISJOINT { 34 | return true 35 | } 36 | } 37 | return false 38 | } 39 | 40 | // IsEqual tests two Well Formed Names for equality. 41 | // @param source Source WFN 42 | // @param target Target WFN 43 | // @return true if the names are equal, false otherwise 44 | func IsEqual(source, target common.WellFormedName) bool { 45 | // if every pairwise comparison is equal, the names are equal. 46 | results := CompareWFNs(source, target) 47 | for _, result := range results { 48 | if result != EQUAL { 49 | return false 50 | } 51 | } 52 | return true 53 | } 54 | 55 | // IsSubset tests if the source Well Formed Name is a subset of the target Well Formed 56 | // Name. 57 | // @param source Source WFN 58 | // @param target Target WFN 59 | // @return true if the source is a subset of the target, false otherwise 60 | func IsSubset(source, target common.WellFormedName) bool { 61 | // if any comparison is anything other than subset or equal, then target is 62 | // not a subset of source. 63 | results := CompareWFNs(source, target) 64 | for _, result := range results { 65 | if result != SUBSET && result != EQUAL { 66 | return false 67 | } 68 | } 69 | return true 70 | } 71 | 72 | // IsSuperset tests if the source Well Formed name is a superset of the target Well Formed Name. 73 | // @param source Source WFN 74 | // @param target Target WFN 75 | // @return true if the source is a superset of the target, false otherwise 76 | func IsSuperset(source, target common.WellFormedName) bool { 77 | // if any comparison is anything other than superset or equal, then target is not 78 | // a superset of source. 79 | results := CompareWFNs(source, target) 80 | for _, result := range results { 81 | if result != SUPERSET && result != EQUAL { 82 | return false 83 | } 84 | } 85 | return true 86 | } 87 | 88 | // CompareWFNs compares each attribute value pair in two Well Formed Names. 89 | // @param source Source WFN 90 | // @param target Target WFN 91 | // @return A Hashtable mapping attribute string to attribute value Relation 92 | func CompareWFNs(source, target common.WellFormedName) map[string]Relation { 93 | result := map[string]Relation{} 94 | result[common.AttributePart] = compare(source.Get(common.AttributePart), target.Get(common.AttributePart)) 95 | result[common.AttributeVendor] = compare(source.Get(common.AttributeVendor), target.Get(common.AttributeVendor)) 96 | result[common.AttributeProduct] = compare(source.Get(common.AttributeProduct), target.Get(common.AttributeProduct)) 97 | result[common.AttributeVersion] = compare(source.Get(common.AttributeVersion), target.Get(common.AttributeVersion)) 98 | result[common.AttributeUpdate] = compare(source.Get(common.AttributeUpdate), target.Get(common.AttributeUpdate)) 99 | result[common.AttributeEdition] = compare(source.Get(common.AttributeEdition), target.Get(common.AttributeEdition)) 100 | result[common.AttributeLanguage] = compare(source.Get(common.AttributeLanguage), target.Get(common.AttributeLanguage)) 101 | result[common.AttributeSwEdition] = compare(source.Get(common.AttributeSwEdition), target.Get(common.AttributeSwEdition)) 102 | result[common.AttributeTargetSw] = compare(source.Get(common.AttributeTargetSw), target.Get(common.AttributeTargetSw)) 103 | result[common.AttributeTargetHw] = compare(source.Get(common.AttributeTargetHw), target.Get(common.AttributeTargetHw)) 104 | result[common.AttributeOther] = compare(source.Get(common.AttributeOther), target.Get(common.AttributeOther)) 105 | return result 106 | } 107 | 108 | // Compares an attribute value pair. 109 | // @param source Source attribute value. 110 | // @param target Target attribute value. 111 | // @return The relation between the two attribute values. 112 | func compare(source, target interface{}) Relation { 113 | var s, t string 114 | var ok bool 115 | 116 | // matching is case insensitive, convert strings to lowercase. 117 | if s, ok = source.(string); ok { 118 | s = strings.ToLower(s) 119 | } 120 | if t, ok = target.(string); ok { 121 | t = strings.ToLower(t) 122 | } 123 | // Unquoted wildcard characters yield an undefined result. 124 | if common.ContainsWildcards(t) { 125 | return UNDEFINED 126 | } 127 | 128 | // If source and target values are equal, then result is equal. 129 | if source == target { 130 | return EQUAL 131 | } 132 | 133 | // Check to see if source or target are Logical Values. 134 | var lvSource, lvTarget common.LogicalValue 135 | if lv, ok := source.(common.LogicalValue); ok { 136 | lvSource = lv 137 | } 138 | if lv, ok := target.(common.LogicalValue); ok { 139 | lvTarget = lv 140 | } 141 | // If source value is ANY, result is a superset. 142 | if lvSource.IsANY() { 143 | return SUPERSET 144 | } 145 | // If target value is ANY, result is a subset. 146 | if lvTarget.IsANY() { 147 | return SUBSET 148 | } 149 | // If source or target is NA, result is disjoint. 150 | if lvSource.IsNA() { 151 | return DISJOINT 152 | } 153 | // if (lvTarget != null) { 154 | if lvTarget.IsNA() { 155 | return DISJOINT 156 | } 157 | // only Strings will get to this point, not LogicalValues 158 | return compareStrings(s, t) 159 | } 160 | 161 | // compareStrings compares a source string to a target string, and addresses the condition 162 | // in which the source string includes unquoted special characters. It 163 | // performs a simple regular expression match, with the assumption that 164 | // (as required) unquoted special characters appear only at the beginning 165 | // and/or the end of the source string. It also properly differentiates 166 | // between unquoted and quoted special characters. 167 | // 168 | // @return Relation between source and target Strings. 169 | func compareStrings(source, target string) Relation { 170 | var start, begins, ends, index, leftover, escapes int 171 | end := len(source) 172 | 173 | if source[0] == '*' { 174 | start = 1 175 | begins = -1 176 | } else { 177 | for start < len(source) && source[start] == '?' { 178 | start++ 179 | begins++ 180 | } 181 | } 182 | 183 | if source[end-1] == '*' && IsEvenWildcards(source, end-1) { 184 | end-- 185 | ends = -1 186 | } else { 187 | for end > 0 && source[end-1] == '?' && IsEvenWildcards(source, end-1) { 188 | end-- 189 | ends++ 190 | } 191 | } 192 | 193 | // only ? (e.g. "???") 194 | if strings.Trim(source, "?") == "" { 195 | if len(source) >= common.LengthWithEscapeCharacters(target) { 196 | return SUPERSET 197 | } 198 | return DISJOINT 199 | } 200 | source = source[start:end] 201 | index = -1 202 | leftover = len(target) 203 | for leftover > 0 { 204 | index = common.IndexOf(target, source, index+1) 205 | if index == -1 { 206 | break 207 | } 208 | escapes = common.CountEscapeCharacters(target[:index]) 209 | if index > 0 && begins != -1 && begins < (index-escapes) { 210 | break 211 | } 212 | escapes = common.CountEscapeCharacters(target[index+1:]) 213 | leftover = len(target) - index - escapes - len(source) 214 | if leftover > 0 && ends != -1 && leftover > ends { 215 | continue 216 | } 217 | return SUPERSET 218 | } 219 | return DISJOINT 220 | } 221 | 222 | // IsEvenWildcards searches a string for the backslash character 223 | // @param str string to search in 224 | // @param idx end index 225 | // @return true if the number of backslash characters is even, false if odd 226 | func IsEvenWildcards(str string, idx int) bool { 227 | result := 0 228 | for idx > 0 && str[idx-1] == '\\' { 229 | idx-- 230 | result++ 231 | } 232 | return result%2 == 0 233 | } 234 | -------------------------------------------------------------------------------- /matching/cpe_name_matcher_test.go: -------------------------------------------------------------------------------- 1 | package matching 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/knqyf263/go-cpe/common" 7 | ) 8 | 9 | func TestMatcher(t *testing.T) { 10 | any, _ := common.NewLogicalValue("ANY") 11 | na, _ := common.NewLogicalValue("NA") 12 | vectors := []struct { 13 | wfn common.WellFormedName 14 | wfn2 common.WellFormedName 15 | expectedIsDisjoint bool 16 | expectedIsEqual bool 17 | expectedIsSubset bool 18 | expectedIsSuperset bool 19 | }{{ 20 | wfn: common.WellFormedName{ 21 | "part": "a", 22 | "vendor": `microsoft`, 23 | "product": "internet_explorer", 24 | "version": `8\.0\.6001`, 25 | "update": "beta", 26 | "edition": any, 27 | "sw_edition": any, 28 | "target_sw": any, 29 | "target_hw": any, 30 | "other": any, 31 | "language": "sp2", 32 | }, 33 | wfn2: common.WellFormedName{ 34 | "part": "a", 35 | "vendor": `microsoft`, 36 | "product": "internet_explorer", 37 | "version": any, 38 | "update": "beta", 39 | "edition": any, 40 | "sw_edition": any, 41 | "target_sw": any, 42 | "target_hw": any, 43 | "other": any, 44 | "language": any, 45 | }, 46 | expectedIsDisjoint: false, 47 | expectedIsEqual: false, 48 | expectedIsSubset: true, 49 | expectedIsSuperset: false, 50 | }, { 51 | wfn: common.WellFormedName{ 52 | "part": "a", 53 | "vendor": `adobe`, 54 | "product": any, 55 | "version": `9\.*`, 56 | "update": any, 57 | "edition": "PalmOS", 58 | "sw_edition": any, 59 | "target_sw": any, 60 | "target_hw": any, 61 | "other": any, 62 | "language": any, 63 | }, 64 | wfn2: common.WellFormedName{ 65 | "part": "a", 66 | "vendor": any, 67 | "product": "reader", 68 | "version": `9\.3\.2`, 69 | "update": na, 70 | "edition": na, 71 | "sw_edition": any, 72 | "target_sw": any, 73 | "target_hw": any, 74 | "other": any, 75 | "language": any, 76 | }, 77 | expectedIsDisjoint: true, 78 | expectedIsEqual: false, 79 | expectedIsSubset: false, 80 | expectedIsSuperset: false, 81 | }, { 82 | wfn: common.WellFormedName{ 83 | "part": "a", 84 | "vendor": `adobe`, 85 | "product": "reader", 86 | "version": `9\.*`, 87 | "update": na, 88 | "edition": na, 89 | "sw_edition": any, 90 | "target_sw": any, 91 | "target_hw": any, 92 | "other": any, 93 | "language": any, 94 | }, 95 | wfn2: common.WellFormedName{ 96 | "part": "a", 97 | "vendor": `adobe`, 98 | "product": "reader", 99 | "version": `8\.3\.2`, 100 | "update": na, 101 | "edition": na, 102 | "sw_edition": any, 103 | "target_sw": any, 104 | "target_hw": any, 105 | "other": any, 106 | "language": any, 107 | }, 108 | expectedIsDisjoint: true, 109 | expectedIsEqual: false, 110 | expectedIsSubset: false, 111 | expectedIsSuperset: false, 112 | }, { 113 | wfn: common.WellFormedName{ 114 | "part": "o", 115 | "vendor": `microsoft`, 116 | "product": `windows_?`, 117 | "version": any, 118 | "update": any, 119 | "edition": any, 120 | "sw_edition": `home*`, 121 | "target_sw": na, 122 | "target_hw": `x64`, 123 | "other": na, 124 | "language": `en\-us`, 125 | }, 126 | wfn2: common.WellFormedName{ 127 | "part": "o", 128 | "vendor": `microsoft`, 129 | "product": `windows_7`, 130 | "version": `6\.1`, 131 | "update": "sp1", 132 | "edition": any, 133 | "sw_edition": `home_basic`, 134 | "target_sw": na, 135 | "target_hw": `x32`, 136 | "other": any, 137 | "language": `en\-us`, 138 | }, 139 | expectedIsDisjoint: true, 140 | expectedIsEqual: false, 141 | expectedIsSubset: false, 142 | expectedIsSuperset: false, 143 | }, { 144 | wfn: common.WellFormedName{ 145 | "part": "o", 146 | "vendor": `microsoft`, 147 | "product": `windows_?`, 148 | "version": any, 149 | "update": any, 150 | "edition": any, 151 | "sw_edition": `home*`, 152 | "target_sw": na, 153 | "target_hw": `x64`, 154 | "other": na, 155 | "language": `en\-us`, 156 | }, 157 | wfn2: common.WellFormedName{ 158 | "part": "o", 159 | "vendor": `microsoft`, 160 | "product": `windows_7`, 161 | "version": `6\.1`, 162 | "update": "sp1", 163 | "edition": any, 164 | "sw_edition": `home_basic`, 165 | "target_sw": na, 166 | "target_hw": `x64`, 167 | "other": na, 168 | "language": `en\-us`, 169 | }, 170 | expectedIsDisjoint: false, 171 | expectedIsEqual: false, 172 | expectedIsSubset: false, 173 | expectedIsSuperset: true, 174 | }, { 175 | wfn: common.WellFormedName{ 176 | "part": "o", 177 | "vendor": `microsoft`, 178 | "product": `windows_7`, 179 | "version": any, 180 | "update": any, 181 | "edition": any, 182 | "sw_edition": `home_basic`, 183 | "target_sw": na, 184 | "target_hw": `x64`, 185 | "other": na, 186 | "language": `en\-us`, 187 | }, 188 | wfn2: common.WellFormedName{ 189 | "part": "o", 190 | "vendor": `microsoft`, 191 | "product": `windows_7`, 192 | "version": any, 193 | "update": any, 194 | "edition": any, 195 | "sw_edition": `home_basic`, 196 | "target_sw": na, 197 | "target_hw": `x64`, 198 | "other": na, 199 | "language": `en\-us`, 200 | }, 201 | expectedIsDisjoint: false, 202 | expectedIsEqual: true, 203 | expectedIsSubset: true, 204 | expectedIsSuperset: true, 205 | }, { 206 | wfn: common.WellFormedName{ 207 | "part": "o", 208 | "vendor": `microsoft`, 209 | }, 210 | wfn2: common.WellFormedName{ 211 | "part": "o", 212 | "vendor": `microsoft*`, 213 | }, 214 | expectedIsDisjoint: false, 215 | expectedIsEqual: false, 216 | expectedIsSubset: false, 217 | expectedIsSuperset: false, 218 | }, { 219 | wfn: common.WellFormedName{ 220 | "part": "o", 221 | "vendor": na, 222 | }, 223 | wfn2: common.WellFormedName{ 224 | "part": "o", 225 | "vendor": `microsoft`, 226 | }, 227 | expectedIsDisjoint: true, 228 | expectedIsEqual: false, 229 | expectedIsSubset: false, 230 | expectedIsSuperset: false, 231 | }, { 232 | wfn: common.WellFormedName{ 233 | "part": "o", 234 | "vendor": "*soft", 235 | }, 236 | wfn2: common.WellFormedName{ 237 | "part": "o", 238 | "vendor": `microsoft`, 239 | }, 240 | expectedIsDisjoint: false, 241 | expectedIsEqual: false, 242 | expectedIsSubset: false, 243 | expectedIsSuperset: true, 244 | }, { 245 | wfn: common.WellFormedName{ 246 | "part": "o", 247 | "vendor": "?icrosoft", 248 | }, 249 | wfn2: common.WellFormedName{ 250 | "part": "o", 251 | "vendor": `microsoft`, 252 | }, 253 | expectedIsDisjoint: false, 254 | expectedIsEqual: false, 255 | expectedIsSubset: false, 256 | expectedIsSuperset: true, 257 | }, { 258 | wfn: common.WellFormedName{ 259 | "part": "o", 260 | "vendor": "?crosoft", 261 | }, 262 | wfn2: common.WellFormedName{ 263 | "part": "o", 264 | "vendor": `microsoft`, 265 | }, 266 | expectedIsDisjoint: true, 267 | expectedIsEqual: false, 268 | expectedIsSubset: false, 269 | expectedIsSuperset: false, 270 | }, { 271 | wfn: common.WellFormedName{ 272 | "part": "o", 273 | "vendor": "??icrosoft", 274 | }, 275 | wfn2: common.WellFormedName{ 276 | "part": "o", 277 | "vendor": `\!\#icrosoft`, 278 | }, 279 | expectedIsDisjoint: false, 280 | expectedIsEqual: false, 281 | expectedIsSubset: false, 282 | expectedIsSuperset: true, 283 | }, { 284 | wfn: common.WellFormedName{ 285 | "part": "o", 286 | "vendor": "microso??", 287 | }, 288 | wfn2: common.WellFormedName{ 289 | "part": "o", 290 | "vendor": `microso\!\#`, 291 | }, 292 | expectedIsDisjoint: false, 293 | expectedIsEqual: false, 294 | expectedIsSubset: false, 295 | expectedIsSuperset: true, 296 | }, { 297 | wfn: common.WellFormedName{ 298 | "part": "o", 299 | "vendor": "microso??", 300 | }, 301 | wfn2: common.WellFormedName{ 302 | "part": "o", 303 | "vendor": `microso\!\#ft`, 304 | }, 305 | expectedIsDisjoint: true, 306 | expectedIsEqual: false, 307 | expectedIsSubset: false, 308 | expectedIsSuperset: false, 309 | }, { 310 | wfn: common.WellFormedName{ 311 | "part": "o", 312 | "vendor": "?", 313 | }, 314 | wfn2: common.WellFormedName{ 315 | "part": "o", 316 | "vendor": `a`, 317 | }, 318 | expectedIsDisjoint: false, 319 | expectedIsEqual: false, 320 | expectedIsSubset: false, 321 | expectedIsSuperset: true, 322 | }, { 323 | wfn: common.WellFormedName{ 324 | "part": "o", 325 | "vendor": "???", 326 | }, 327 | wfn2: common.WellFormedName{ 328 | "part": "o", 329 | "vendor": `ab`, 330 | }, 331 | expectedIsDisjoint: false, 332 | expectedIsEqual: false, 333 | expectedIsSubset: false, 334 | expectedIsSuperset: true, 335 | }, { 336 | wfn: common.WellFormedName{ 337 | "part": "o", 338 | "vendor": "?", 339 | }, 340 | wfn2: common.WellFormedName{ 341 | "part": "o", 342 | "vendor": `ab`, 343 | }, 344 | expectedIsDisjoint: true, 345 | expectedIsEqual: false, 346 | expectedIsSubset: false, 347 | expectedIsSuperset: false, 348 | }, { 349 | wfn: common.WellFormedName{ 350 | "part": "a", 351 | "version": "3", 352 | }, 353 | wfn2: common.WellFormedName{ 354 | "part": "a", 355 | "version": "33.0.1750.126", 356 | }, 357 | expectedIsDisjoint: true, 358 | expectedIsEqual: false, 359 | expectedIsSubset: false, 360 | expectedIsSuperset: false, 361 | }, 362 | } 363 | 364 | var actual bool 365 | for i, v := range vectors { 366 | actual = IsDisjoint(v.wfn, v.wfn2) 367 | if actual != v.expectedIsDisjoint { 368 | t.Errorf("test %d, IsDisJoint: got %v, want %v", i, actual, v.expectedIsDisjoint) 369 | } 370 | actual = IsEqual(v.wfn, v.wfn2) 371 | if actual != v.expectedIsEqual { 372 | t.Errorf("test %d, IsEqual: got %v, want %v", i, actual, v.expectedIsEqual) 373 | } 374 | actual = IsSubset(v.wfn, v.wfn2) 375 | if actual != v.expectedIsSubset { 376 | t.Errorf("test %d, IsSubset: got %v, want %v", i, actual, v.expectedIsSubset) 377 | } 378 | actual = IsSuperset(v.wfn, v.wfn2) 379 | if actual != v.expectedIsSuperset { 380 | t.Errorf("test %d, IsSuperset: got %v, want %v", i, actual, v.expectedIsSuperset) 381 | } 382 | } 383 | } 384 | 385 | func TestIsEvenWildcards(t *testing.T) { 386 | vectors := []struct { 387 | str string 388 | index int 389 | expected bool 390 | }{{ 391 | str: `abc`, 392 | index: 2, 393 | expected: true, 394 | }, { 395 | str: `abc*`, 396 | index: 3, 397 | expected: true, 398 | }, { 399 | str: `abc\*`, 400 | index: 4, 401 | expected: false, 402 | }, { 403 | str: `abc\\*`, 404 | index: 5, 405 | expected: true, 406 | }, { 407 | str: `abc\\\*`, 408 | index: 6, 409 | expected: false, 410 | }, { 411 | str: `abc\\def\*`, 412 | index: 9, 413 | expected: false, 414 | }, 415 | } 416 | 417 | for i, v := range vectors { 418 | actual := IsEvenWildcards(v.str, v.index) 419 | if actual != v.expected { 420 | t.Errorf("test %d, Result: got %v, want %v", i, actual, v.expected) 421 | } 422 | } 423 | } 424 | -------------------------------------------------------------------------------- /naming/cpe_name_binder.go: -------------------------------------------------------------------------------- 1 | package naming 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/knqyf263/go-cpe/common" 8 | ) 9 | 10 | // BindToURI a {@link WellFormedName} object to a URI. 11 | // @param w WellFormedName to be bound to URI 12 | // @return URI binding of WFN 13 | func BindToURI(w common.WellFormedName) (uri string) { 14 | // Initialize the output with the CPE v2.2 URI prefix. 15 | uri = "cpe:/" 16 | 17 | // Define the attributes that correspond to the seven components in a v2.2. CPE. 18 | attributes := []string{"part", "vendor", "product", "version", "update", "edition", "language"} 19 | 20 | for _, a := range attributes { 21 | v := "" 22 | if a == "edition" { 23 | // Call the pack() helper function to compute the proper binding for the edition element. 24 | ed := bindValueForURI(w.Get(common.AttributeEdition)) 25 | swEd := bindValueForURI(w.Get(common.AttributeSwEdition)) 26 | targetSw := bindValueForURI(w.Get(common.AttributeTargetSw)) 27 | targetHw := bindValueForURI(w.Get(common.AttributeTargetHw)) 28 | other := bindValueForURI(w.Get(common.AttributeOther)) 29 | v = pack(ed, swEd, targetSw, targetHw, other) 30 | } else { 31 | // Get the value for a in w, then bind to a string 32 | // for inclusion in the URI. 33 | v = bindValueForURI(w.Get(a)) 34 | } 35 | // Append v to the URI then add a colon. 36 | uri += v + ":" 37 | } 38 | return strings.TrimRight(uri, ":") 39 | } 40 | 41 | // BindToFS is top-level function used to bind WFN w to formatted string. 42 | // @param w WellFormedName to bind 43 | // @return Formatted String 44 | func BindToFS(w common.WellFormedName) (fs string) { 45 | // Initialize the output with the CPE v2.3 string prefix. 46 | fs = "cpe:2.3:" 47 | attributes := []string{"part", "vendor", "product", "version", 48 | "update", "edition", "language", "sw_edition", "target_sw", 49 | "target_hw", "other"} 50 | for _, a := range attributes { 51 | v := bindValueForFS(w.Get(a)) 52 | fs += v 53 | if a != common.AttributeOther { 54 | fs += ":" 55 | } 56 | } 57 | return fs 58 | } 59 | 60 | // bindValueForURI converts a string to the proper string for including in a CPE v2.2-conformant URI. 61 | // The logical value ANY binds to the blank in the 2.2-conformant URI. 62 | // @param s string to be converted 63 | // @return converted string 64 | func bindValueForURI(s interface{}) string { 65 | if lv, ok := s.(common.LogicalValue); ok { 66 | // The value NA binds to a blank. 67 | if lv.IsANY() { 68 | return "" 69 | } 70 | // The value NA binds to a single hyphen. 71 | if lv.IsNA() { 72 | return "-" 73 | } 74 | } 75 | 76 | if str, ok := s.(string); ok { 77 | return transformForURI(str) 78 | } 79 | return "" 80 | } 81 | 82 | // bindValueForFS converts the value v to its proper string representation for insertion to formatted string. 83 | // @param v value to convert 84 | // @return Formatted value 85 | func bindValueForFS(v interface{}) string { 86 | if lv, ok := v.(common.LogicalValue); ok { 87 | // The value NA binds to a asterisk. 88 | if lv.IsANY() { 89 | return "*" 90 | } 91 | // The value NA binds to a single hyphen. 92 | if lv.IsNA() { 93 | return "-" 94 | } 95 | } 96 | if str, ok := v.(string); ok { 97 | return processQuotedChars(str) 98 | } 99 | return "" 100 | } 101 | 102 | // Inspect each character in string s. Certain nonalpha characters pass 103 | // thru without escaping into the result, but most retain escaping. 104 | // @param s 105 | // @return 106 | func processQuotedChars(s string) (result string) { 107 | idx := 0 108 | for idx < len(s) { 109 | c := s[idx : idx+1] 110 | if c == "\\" { 111 | // escaped characters are examined. 112 | nextchr := s[idx+1 : idx+2] 113 | // the period, hyphen and underscore pass unharmed. 114 | if nextchr == "." || nextchr == "-" || nextchr == "_" { 115 | result += nextchr 116 | idx += 2 117 | continue 118 | } else { 119 | // all others retain escaping. 120 | result += "\\" + nextchr 121 | idx += 2 122 | continue 123 | } 124 | } 125 | // unquoted characters pass thru unharmed. 126 | result += c 127 | idx = idx + 1 128 | } 129 | return result 130 | } 131 | 132 | // transformForURI scans an input string and performs the following transformations: 133 | // - Pass alphanumeric characters thru untouched 134 | // - Percent-encode quoted non-alphanumerics as needed 135 | // - Unquoted special characters are mapped to their special forms 136 | // @param s string to be transformed 137 | // @return transformed string 138 | func transformForURI(s string) (result string) { 139 | idx := 0 140 | for idx < len(s) { 141 | // Get the idx'th character of s. 142 | thischar := s[idx : idx+1] 143 | if common.IsAlphanum(thischar) { 144 | result += thischar 145 | idx++ 146 | continue 147 | } 148 | // Check for escape character. 149 | if thischar == "\\" { 150 | idx++ 151 | nxtchar := s[idx : idx+1] 152 | result += pctEncode(nxtchar) 153 | idx++ 154 | continue 155 | } 156 | // Bind the unquoted '?' special character to "%01". 157 | if thischar == "?" { 158 | result += "%01" 159 | } 160 | // Bind the unquoted '*' special character to "%02". 161 | if thischar == "*" { 162 | result += "%02" 163 | } 164 | idx++ 165 | 166 | } 167 | return result 168 | } 169 | 170 | // pctEncode returns the appropriate percent-encoding of character c. 171 | // Certain characters are returned without encoding. 172 | // @param c the single character string to be encoded 173 | // @return the percent encoded string 174 | func pctEncode(c string) string { 175 | if c == "!" { 176 | return "%21" 177 | } 178 | if c == "\"" { 179 | return "%22" 180 | } 181 | if c == "#" { 182 | return "%23" 183 | } 184 | if c == "$" { 185 | return "%24" 186 | } 187 | if c == "%" { 188 | return "%25" 189 | } 190 | if c == "&" { 191 | return "%26" 192 | } 193 | if c == "'" { 194 | return "%27" 195 | } 196 | if c == "(" { 197 | return "%28" 198 | } 199 | if c == ")" { 200 | return "%29" 201 | } 202 | if c == "*" { 203 | return "%2a" 204 | } 205 | if c == "+" { 206 | return "%2b" 207 | } 208 | if c == "," { 209 | return "%2c" 210 | } 211 | // bound without encoding. 212 | if c == "-" { 213 | return c 214 | } 215 | // bound without encoding. 216 | if c == "." { 217 | return c 218 | } 219 | if c == "/" { 220 | return "%2f" 221 | } 222 | if c == ":" { 223 | return "%3a" 224 | } 225 | if c == ";" { 226 | return "%3b" 227 | } 228 | if c == "<" { 229 | return "%3c" 230 | } 231 | if c == "=" { 232 | return "%3d" 233 | } 234 | if c == ">" { 235 | return "%3e" 236 | } 237 | if c == "?" { 238 | return "%3f" 239 | } 240 | if c == "@" { 241 | return "%40" 242 | } 243 | if c == "[" { 244 | return "%5b" 245 | } 246 | if c == "\\" { 247 | return "%5c" 248 | } 249 | if c == "]" { 250 | return "%5d" 251 | } 252 | if c == "^" { 253 | return "%5e" 254 | } 255 | if c == "`" { 256 | return "%60" 257 | } 258 | if c == "{" { 259 | return "%7b" 260 | } 261 | if c == "|" { 262 | return "%7c" 263 | } 264 | if c == "}" { 265 | return "%7d" 266 | } 267 | if c == "~" { 268 | return "%7e" 269 | } 270 | // Shouldn't reach here, return original character 271 | return c 272 | } 273 | 274 | /** 275 | * Packs the values of the five arguments into the single 276 | * edition component. If all the values are blank, the 277 | * function returns a blank. 278 | * @param ed edition string 279 | * @param swEd software edition string 280 | * @param tSw target software string 281 | * @param tHw target hardware string 282 | * @param oth other edition information string 283 | * @return the packed string, or blank 284 | */ 285 | func pack(ed, swEd, tSw, tHw, oth string) string { 286 | if swEd == "" && tSw == "" && tHw == "" && oth == "" { 287 | // All the extended attributes are blank, so don't do 288 | // any packing, just return ed. 289 | return ed 290 | } 291 | // Otherwise, pack the five values into a single string 292 | // prefixed and internally delimited with the tilde. 293 | return fmt.Sprintf("~%s~%s~%s~%s~%s", ed, swEd, tSw, tHw, oth) 294 | } 295 | -------------------------------------------------------------------------------- /naming/cpe_name_binder_test.go: -------------------------------------------------------------------------------- 1 | package naming 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/knqyf263/go-cpe/common" 7 | ) 8 | 9 | var ( 10 | any, _ = common.NewLogicalValue("ANY") 11 | na, _ = common.NewLogicalValue("NA") 12 | ) 13 | 14 | func TestBindToURI(t *testing.T) { 15 | vectors := []struct { 16 | w common.WellFormedName 17 | expected string 18 | }{{ 19 | w: common.WellFormedName{ 20 | "part": "a", 21 | }, 22 | expected: "cpe:/a", 23 | }, { 24 | w: common.WellFormedName{ 25 | "part": "a", 26 | "vendor": "microsoft", 27 | "product": "internet_explorer", 28 | "version": "8\\.0\\.6001", 29 | "update": "beta", 30 | "edition": any, 31 | "language": "sp2", 32 | }, 33 | expected: "cpe:/a:microsoft:internet_explorer:8.0.6001:beta::sp2", 34 | }, { 35 | w: common.WellFormedName{ 36 | "part": "a", 37 | "vendor": "foo\\$bar", 38 | "product": "insight", 39 | "version": "7\\.4\\.0\\.1570", 40 | "update": na, 41 | "sw_edition": "online", 42 | "target_sw": "win2003", 43 | "target_hw": "x64", 44 | }, 45 | expected: "cpe:/a:foo%24bar:insight:7.4.0.1570:-:~~online~win2003~x64~", 46 | }, { 47 | w: common.WellFormedName{ 48 | "part": "a", 49 | "vendor": 1, 50 | }, 51 | expected: "cpe:/a", 52 | }, { 53 | w: common.WellFormedName{ 54 | "part": "a", 55 | "vendor": "micro??", 56 | "product": "internet*", 57 | "version": `\!\"\#\$\%\&\'\(\)\*\+\,`, 58 | "update": `beta\-\.`, 59 | "language": `online\/\:\;\<\=\>\?\@\[\\\]\^`, 60 | }, 61 | expected: "cpe:/a:micro%01%01:internet%02:%21%22%23%24%25%26%27%28%29%2a%2b%2c:beta-.::online%2f%3a%3b%3c%3d%3e%3f%40%5b%5c%5d%5e", 62 | }, { 63 | w: common.WellFormedName{ 64 | "part": "a", 65 | "vendor": "\\`", 66 | "product": `\{\|\}\~\a`, 67 | }, 68 | expected: "cpe:/a:%60:%7b%7c%7d%7ea", 69 | }, 70 | } 71 | 72 | for i, v := range vectors { 73 | actual := BindToURI(v.w) 74 | if actual != v.expected { 75 | t.Errorf("test %d, Result: got %v, want %v", i, actual, v.expected) 76 | } 77 | } 78 | } 79 | 80 | func TestBindToFS(t *testing.T) { 81 | vectors := []struct { 82 | w common.WellFormedName 83 | expected string 84 | }{{ 85 | w: common.WellFormedName{ 86 | "part": "a", 87 | }, 88 | expected: "cpe:2.3:a:*:*:*:*:*:*:*:*:*:*", 89 | }, { 90 | w: common.WellFormedName{ 91 | "part": "a", 92 | "vendor": "microsoft", 93 | "product": "internet_explorer", 94 | "version": "8\\.0\\.6001", 95 | "update": "beta", 96 | "edition": any, 97 | "language": "sp2", 98 | }, 99 | expected: "cpe:2.3:a:microsoft:internet_explorer:8.0.6001:beta:*:sp2:*:*:*:*", 100 | }, { 101 | w: common.WellFormedName{ 102 | "part": "a", 103 | "vendor": "foo\\$bar", 104 | "product": "insight", 105 | "version": "7\\.4\\.0\\.1570", 106 | "update": na, 107 | "sw_edition": "online", 108 | "target_sw": "win2003", 109 | "target_hw": "x64", 110 | }, 111 | expected: "cpe:2.3:a:foo\\$bar:insight:7.4.0.1570:-:*:*:online:win2003:x64:*", 112 | }, { 113 | w: common.WellFormedName{ 114 | "part": "a", 115 | "vendor": 1, 116 | }, 117 | expected: "cpe:2.3:a::*:*:*:*:*:*:*:*:*", 118 | }, 119 | } 120 | 121 | for i, v := range vectors { 122 | actual := BindToFS(v.w) 123 | if actual != v.expected { 124 | t.Errorf("test %d, Result: got %v, want %v", i, actual, v.expected) 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /naming/cpe_name_unbinder.go: -------------------------------------------------------------------------------- 1 | package naming 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/knqyf263/go-cpe/common" 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | // UnbindURI is a top level function used to unbind a URI to a WFN. 11 | // @param uri String representing the URI to be unbound. 12 | // @return WellFormedName representing the unbound URI. 13 | func UnbindURI(uri string) (common.WellFormedName, error) { 14 | if err := common.ValidateURI(uri); err != nil { 15 | return nil, errors.Wrap(err, "Failed to validate uri") 16 | } 17 | result := common.WellFormedName{} 18 | for i := 0; i < 8; i++ { 19 | v := getCompURI(uri, i) 20 | d, err := decode(v) 21 | if i != 6 && err != nil { 22 | return nil, err 23 | } 24 | switch i { 25 | case 1: 26 | err = result.Set(common.AttributePart, d) 27 | case 2: 28 | err = result.Set(common.AttributeVendor, d) 29 | case 3: 30 | err = result.Set(common.AttributeProduct, d) 31 | case 4: 32 | err = result.Set(common.AttributeVersion, d) 33 | case 5: 34 | err = result.Set(common.AttributeUpdate, d) 35 | case 6: 36 | // Special handling for edition component. 37 | // Unpack edition if needed. 38 | if v == "" || v == "-" || v[0] != '~' { 39 | // Just a logical value or a non-packed value. 40 | // So unbind to legacy edition, leaving other 41 | // extended attributes unspecified. 42 | err = result.Set(common.AttributeEdition, d) 43 | } else { 44 | // We have five values packed together here. 45 | if result, err = unpack(v, result); err != nil { 46 | return nil, err 47 | } 48 | } 49 | case 7: 50 | err = result.Set(common.AttributeLanguage, d) 51 | } 52 | if err != nil { 53 | return nil, err 54 | } 55 | } 56 | return result, nil 57 | 58 | } 59 | 60 | // UnbindFS is a top level function to unbind a formatted string to WFN. 61 | // @param fs Formatted string to unbind 62 | // @return WellFormedName 63 | // @throws ParseException 64 | func UnbindFS(fs string) (common.WellFormedName, error) { 65 | // Validate the formatted string 66 | if err := common.ValidateFS(fs); err != nil { 67 | return nil, err 68 | } 69 | // Initialize empty WFN 70 | result := common.NewWellFormedName() 71 | // The cpe scheme is the 0th component, the cpe version is the 1st. 72 | // So we start parsing at the 2nd component. 73 | for a := 2; a != 13; a++ { 74 | // Get the a'th string field. 75 | s := getCompFS(fs, a) 76 | // Unbind the string. 77 | v, err := unbindValueFS(s) 78 | if err != nil { 79 | return nil, err 80 | } 81 | 82 | // Set the value of the corresponding attribute. 83 | switch a { 84 | case 2: 85 | err = result.Set(common.AttributePart, v) 86 | break 87 | case 3: 88 | err = result.Set(common.AttributeVendor, v) 89 | break 90 | case 4: 91 | err = result.Set(common.AttributeProduct, v) 92 | break 93 | case 5: 94 | err = result.Set(common.AttributeVersion, v) 95 | break 96 | case 6: 97 | err = result.Set(common.AttributeUpdate, v) 98 | break 99 | case 7: 100 | err = result.Set(common.AttributeEdition, v) 101 | break 102 | case 8: 103 | err = result.Set(common.AttributeLanguage, v) 104 | break 105 | case 9: 106 | err = result.Set(common.AttributeSwEdition, v) 107 | break 108 | case 10: 109 | err = result.Set(common.AttributeTargetSw, v) 110 | break 111 | case 11: 112 | err = result.Set(common.AttributeTargetHw, v) 113 | break 114 | case 12: 115 | err = result.Set(common.AttributeOther, v) 116 | break 117 | } 118 | if err != nil { 119 | return nil, err 120 | } 121 | } 122 | return result, nil 123 | } 124 | 125 | // getCompURI return the i'th component of the URI. 126 | // @param uri String representation of URI to retrieve components from. 127 | // @param i Index of component to return. 128 | // @return If i = 0, returns the URI scheme. Otherwise, returns the i'th 129 | // component of uri. 130 | func getCompURI(uri string, i int) string { 131 | if i == 0 { 132 | return uri[:strings.Index(uri, "/")] 133 | } 134 | sa := strings.Split(uri, ":") 135 | // If requested component exceeds the number 136 | // of components in URI, return blank 137 | if i >= len(sa) { 138 | return "" 139 | } 140 | if i == 1 { 141 | return strings.TrimLeft(sa[1], "/") 142 | } 143 | return sa[i] 144 | } 145 | 146 | // Returns the i'th field of the formatted string. The colon is the field 147 | // delimiter unless prefixed by a backslash. 148 | // @param fs formatted string to retrieve from 149 | // @param i index of field to retrieve from fs. 150 | // @return value of index of formatted string 151 | func getCompFS(fs string, i int) string { 152 | if i < 0 { 153 | return "" 154 | } 155 | 156 | for j := 0; j < i; j++ { 157 | // return the substring from index 0 to the first occurence of an 158 | // unescaped colon 159 | colonIdx := common.GetUnescapedColonIndex(fs) 160 | if colonIdx == 0 { 161 | fs = "" 162 | break 163 | } 164 | fs = fs[colonIdx+1:] 165 | } 166 | endIdx := common.GetUnescapedColonIndex(fs) 167 | // If no colon is found, we are at the end of the formatted string, 168 | // so just return what's left. 169 | if endIdx == 0 { 170 | return fs 171 | } 172 | return fs[:endIdx] 173 | } 174 | 175 | // Takes a string value and returns the appropriate logical value if string 176 | // is the bound form of a logical value. If string is some general value 177 | // string, add quoting of non-alphanumerics as needed. 178 | // @param s value to be unbound 179 | // @return logical value or quoted string 180 | // @throws ParseException 181 | func unbindValueFS(s string) (interface{}, error) { 182 | if s == "*" { 183 | any, _ := common.NewLogicalValue("ANY") 184 | return any, nil 185 | } 186 | if s == "-" { 187 | na, _ := common.NewLogicalValue("NA") 188 | return na, nil 189 | } 190 | result, err := addQuoting(s) 191 | if err != nil { 192 | return nil, err 193 | } 194 | return result, nil 195 | } 196 | 197 | // Inspect each character in a string, copying quoted characters, with 198 | // their escaping, into the result. Look for unquoted non alphanumerics 199 | // and if not "*" or "?", add escaping. 200 | // @param s 201 | // @return 202 | // @throws ParseException 203 | func addQuoting(s string) (result string, err error) { 204 | idx := 0 205 | embedded := false 206 | for idx < len(s) { 207 | c := s[idx : idx+1] 208 | if common.IsAlphanum(c) || c == "_" { 209 | // Alphanumeric characters pass untouched. 210 | result += c 211 | idx++ 212 | embedded = true 213 | continue 214 | } 215 | if c == "\\" { 216 | // Anything quoted in the bound string stays quoted in the 217 | // unbound string. 218 | result += s[idx : idx+2] 219 | idx += 2 220 | embedded = true 221 | continue 222 | } 223 | if c == "*" { 224 | // An unquoted asterisk must appear at the beginning or the end 225 | // of the string. 226 | if idx == 0 || idx == len(s)-1 { 227 | result += c 228 | idx = idx + 1 229 | embedded = true 230 | continue 231 | } 232 | return "", errors.Wrap(common.ErrParse, "Error! cannot have unquoted * embedded in formatted string.") 233 | } 234 | if c == "?" { 235 | // An unquoted question mark must appear at the beginning or 236 | // end of the string, or in a leading or trailing sequence. 237 | // if // ? legal at beginning or end 238 | valid := false 239 | if idx == 0 || idx == len(s)-1 { 240 | // ? legal at beginning or end 241 | valid = true 242 | } else if !embedded && idx > 0 && s[idx-1:idx] == "?" { 243 | // embedded is false, so must be preceded by ? 244 | valid = true 245 | } else if embedded && len(s) >= idx+2 && s[idx+1:idx+2] == "?" { 246 | // embedded is true, so must be followed by ? 247 | valid = true 248 | } 249 | 250 | if !valid { 251 | return "", errors.Wrap(common.ErrParse, "Error! cannot have unquoted ? embedded in formatted string.") 252 | } 253 | result += c 254 | idx++ 255 | embedded = false 256 | continue 257 | } 258 | // All other characters must be quoted. 259 | result += "\\" + c 260 | idx++ 261 | embedded = true 262 | } 263 | return result, nil 264 | } 265 | 266 | // decode scans a string and returns a copy with all percent-encoded characters 267 | // decoded. This function is the inverse of pctEncode() defined in the 268 | // CPENameBinder class. Only legal percent-encoded forms are decoded. 269 | // Others raise a ParseException. 270 | // @param s String to be decoded 271 | // @return decoded string 272 | // @throws ParseException 273 | // @see CPENameBinder#pctEncode(java.lang.String) 274 | func decode(s string) (interface{}, error) { 275 | if s == "" { 276 | any, _ := common.NewLogicalValue("ANY") 277 | return any, nil 278 | } 279 | if s == "-" { 280 | na, _ := common.NewLogicalValue("NA") 281 | return na, nil 282 | } 283 | // Start the scanning loop. 284 | // Normalize: convert all uppercase letters to lowercase first. 285 | s = strings.ToLower(s) 286 | result := "" 287 | idx := 0 288 | embedded := false 289 | for idx < len(s) { 290 | // Get the idx'th character of s. 291 | c := s[idx : idx+1] 292 | // Deal with dot, hyphen, and tilde: decode with quoting. 293 | if c == "." || c == "-" || c == "~" { 294 | result += "\\" + c 295 | idx++ 296 | // a non-%01 encountered. 297 | embedded = true 298 | continue 299 | } 300 | if c != "%" { 301 | result += c 302 | idx++ 303 | // a non-%01 encountered. 304 | embedded = true 305 | continue 306 | } 307 | // We get here if we have a substring starting w/ '%'. 308 | form := s[idx : idx+3] 309 | if form == "%01" { 310 | valid := false 311 | if idx == 0 || idx == len(s)-3 { 312 | valid = true 313 | } else if !embedded && (idx-3 >= 0) && s[idx-3:idx] == "%01" { 314 | valid = true 315 | } else if embedded && len(s) >= idx+6 && s[idx+3:idx+6] == "%01" { 316 | valid = true 317 | } 318 | 319 | if valid { 320 | result += "?" 321 | idx = idx + 3 322 | continue 323 | } else { 324 | return nil, errors.Wrapf(common.ErrParse, "Error decoding string") 325 | } 326 | } else if form == "%02" { 327 | if idx == 0 || idx == len(s)-3 { 328 | result += "*" 329 | } else { 330 | return nil, errors.Wrapf(common.ErrParse, "Error decoding string") 331 | } 332 | } else if form == "%21" { 333 | result += `\!` 334 | } else if form == "%22" { 335 | result += `\"` 336 | } else if form == "%23" { 337 | result += `\#` 338 | } else if form == "%24" { 339 | result += `\$` 340 | } else if form == "%25" { 341 | result += `\%` 342 | } else if form == "%26" { 343 | result += `\&` 344 | } else if form == "%27" { 345 | result += `\'` 346 | } else if form == "%28" { 347 | result += `\(` 348 | } else if form == "%29" { 349 | result += `\)` 350 | } else if form == "%2a" { 351 | result += `\*` 352 | } else if form == "%2b" { 353 | result += `\+` 354 | } else if form == "%2c" { 355 | result += `\,` 356 | } else if form == "%2f" { 357 | result += `\/` 358 | } else if form == "%3a" { 359 | result += `\:` 360 | } else if form == "%3b" { 361 | result += `\;` 362 | } else if form == "%3c" { 363 | result += `\<` 364 | } else if form == "%3d" { 365 | result += `\=` 366 | } else if form == "%3e" { 367 | result += `\>` 368 | } else if form == "%3f" { 369 | result += `\?` 370 | } else if form == "%40" { 371 | result += `\@` 372 | } else if form == "%5b" { 373 | result += `\[` 374 | } else if form == "%5c" { 375 | result += `\\` 376 | } else if form == "%5d" { 377 | result += `\]` 378 | } else if form == "%5e" { 379 | result += `\^` 380 | } else if form == "%60" { 381 | result += "\\`" 382 | } else if form == "%7b" { 383 | result += `\{` 384 | } else if form == "%7c" { 385 | result += `\|` 386 | } else if form == "%7d" { 387 | result += `\}` 388 | } else if form == "%7e" { 389 | result += `\~` 390 | } else { 391 | return nil, errors.Wrapf(common.ErrParse, "Unknown form: %s", form) 392 | } 393 | idx = idx + 3 394 | embedded = true 395 | } 396 | return result, nil 397 | } 398 | 399 | // unpack unpacks the elements in s and sets the attributes in the given 400 | // WellFormedName accordingly. 401 | // @param s packed String 402 | // @param wfn WellFormedName 403 | // @return The augmented WellFormedName. 404 | func unpack(s string, wfn common.WellFormedName) (common.WellFormedName, error) { 405 | // Set each component in the WFN. 406 | editions := strings.SplitN(s[1:], "~", 5) 407 | if len(editions) != 5 { 408 | return nil, errors.Wrap(common.ErrParse, "editions must be 5") 409 | } 410 | attributes := []string{common.AttributeEdition, common.AttributeSwEdition, common.AttributeTargetSw, 411 | common.AttributeTargetHw, common.AttributeOther} 412 | for i, a := range attributes { 413 | e, err := decode(editions[i]) 414 | if err != nil { 415 | return nil, err 416 | } 417 | if err = wfn.Set(a, e); err != nil { 418 | return nil, err 419 | } 420 | } 421 | return wfn, nil 422 | } 423 | -------------------------------------------------------------------------------- /naming/cpe_name_unbinder_test.go: -------------------------------------------------------------------------------- 1 | package naming 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/knqyf263/go-cpe/common" 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | func TestUnbindURI(t *testing.T) { 12 | vectors := []struct { 13 | s string 14 | expected common.WellFormedName 15 | wantErr error 16 | }{{ 17 | s: "cpe:/a:microsoft:internet_explorer%01%01%01%01:?:beta", 18 | expected: common.WellFormedName{ 19 | "part": "a", 20 | "vendor": "microsoft", 21 | "product": "internet_explorer????", 22 | "version": "?", 23 | "update": "beta", 24 | "edition": any, 25 | "sw_edition": any, 26 | "target_sw": any, 27 | "target_hw": any, 28 | "other": any, 29 | "language": any, 30 | }, 31 | }, { 32 | s: "cpe:/a:microsoft:internet_explorer:8.%2a:sp%3f", 33 | expected: common.WellFormedName{ 34 | "part": "a", 35 | "vendor": "microsoft", 36 | "product": "internet_explorer", 37 | "version": `8\.\*`, 38 | "update": `sp\?`, 39 | "edition": any, 40 | "sw_edition": any, 41 | "target_sw": any, 42 | "target_hw": any, 43 | "other": any, 44 | "language": any, 45 | }, 46 | }, { 47 | s: "cpe:/a:microsoft:internet_explorer:8.%02:sp%01", 48 | expected: common.WellFormedName{ 49 | "part": "a", 50 | "vendor": "microsoft", 51 | "product": "internet_explorer", 52 | "version": `8\.*`, 53 | "update": `sp?`, 54 | "edition": any, 55 | "sw_edition": any, 56 | "target_sw": any, 57 | "target_hw": any, 58 | "other": any, 59 | "language": any, 60 | }, 61 | }, { 62 | s: "cpe:/a:hp:insight_diagnostics:7.4.0.1570::~~online~win2003~x64~", 63 | expected: common.WellFormedName{ 64 | "part": "a", 65 | "vendor": "hp", 66 | "product": "insight_diagnostics", 67 | "version": `7\.4\.0\.1570`, 68 | "update": any, 69 | "edition": any, 70 | "sw_edition": "online", 71 | "target_sw": "win2003", 72 | "target_hw": "x64", 73 | "other": any, 74 | "language": any, 75 | }, 76 | }, { 77 | s: "cpe:/a:%01%01microsoft", 78 | expected: common.WellFormedName{ 79 | "part": "a", 80 | "vendor": "??microsoft", 81 | }, 82 | }, { 83 | s: "cpe:2.3:a:foo\\$bar:insight:7.4.0.1570:-:*:*:online:win2003:x64:*", 84 | wantErr: common.ErrParse, 85 | }, { 86 | s: "cpe:/a:microsoft:internet_explorer:8.%02:s%01p", 87 | wantErr: common.ErrParse, 88 | }, { 89 | s: "cpe:/a:micro%02soft", 90 | wantErr: common.ErrParse, 91 | }, { 92 | s: "cpe:/a:micro%01%01:internet%02:%21%22%23%24%25%26%27%28%29%2a%2b%2c:beta-.::online%2f%3a%3b%3c%3d%3e%3f%40%5b%5c%5d%5e", 93 | expected: common.WellFormedName{ 94 | "part": "a", 95 | "vendor": "micro??", 96 | "product": "internet*", 97 | "version": `\!\"\#\$\%\&\'\(\)\*\+\,`, 98 | "update": `beta\-\.`, 99 | "language": `online\/\:\;\<\=\>\?\@\[\\\]\^`, 100 | }, 101 | }, { 102 | s: "cpe:/a:%60:%7b%7c%7d%7ea", 103 | expected: common.WellFormedName{ 104 | "part": "a", 105 | "vendor": "\\`", 106 | "product": `\{\|\}\~a`, 107 | }, 108 | }, { 109 | s: "cpe:/a:hp:insight_diagnostics:7.4.0.1570::~~online~win2003", 110 | wantErr: common.ErrParse, 111 | }, { 112 | s: "cpe:/a:hp:insight_diagnostics:7.4.0.1570::~~online~win2003~%99~", 113 | wantErr: common.ErrParse, 114 | }, { 115 | s: `cpe:/a:hp:insight_diagnostics:7.4.0.1570::~~online~win2003~a*b~`, 116 | wantErr: common.ErrParse, 117 | }, { 118 | s: "cpe:/a:%99", 119 | wantErr: common.ErrParse, 120 | }, { 121 | s: "cpe:/a:b*c", 122 | wantErr: common.ErrParse, 123 | }, { 124 | s: "cpe:/z:%01%01microsoft", 125 | wantErr: common.ErrParse, 126 | }, { 127 | // empty part 128 | s: ` cpe:/:microsoft`, 129 | wantErr: common.ErrParse, 130 | }, 131 | } 132 | 133 | for i, v := range vectors { 134 | actual, err := UnbindURI(v.s) 135 | if errors.Cause(err) != v.wantErr { 136 | t.Errorf("test %d, Error: got %v, want %v", i, errors.Cause(err), v.wantErr) 137 | } 138 | if err != nil { 139 | continue 140 | } 141 | if actual.Get("part") != v.expected.Get("part") { 142 | t.Errorf("test %d, part: got %v, want %v", i, actual.Get("part"), v.expected.Get("part")) 143 | } 144 | if actual.Get("vendor") != v.expected.Get("vendor") { 145 | t.Errorf("test %d, vendor: got %v, want %v", i, actual.Get("vendor"), v.expected.Get("vendor")) 146 | } 147 | if actual.Get("product") != v.expected.Get("product") { 148 | t.Errorf("test %d, product: got %v, want %v", i, actual.Get("product"), v.expected.Get("product")) 149 | } 150 | if actual.Get("version") != v.expected.Get("version") { 151 | t.Errorf("test %d, version: got %v, want %v", i, actual.Get("version"), v.expected.Get("version")) 152 | } 153 | if actual.Get("update") != v.expected.Get("update") { 154 | t.Errorf("test %d, update: got %v, want %v", i, actual.Get("update"), v.expected.Get("update")) 155 | } 156 | if actual.Get("edition") != v.expected.Get("edition") { 157 | t.Errorf("test %d, edition: got %v, want %v", i, actual.Get("edition"), v.expected.Get("edition")) 158 | } 159 | if actual.Get("sw_edition") != v.expected.Get("sw_edition") { 160 | t.Errorf("test %d, sw_edition: got %v, want %v", i, actual.Get("sw_edition"), v.expected.Get("sw_edition")) 161 | } 162 | if actual.Get("target_sw") != v.expected.Get("target_sw") { 163 | t.Errorf("test %d, target_sw: got %v, want %v", i, actual.Get("target_sw"), v.expected.Get("target_sw")) 164 | } 165 | if actual.Get("target_hw") != v.expected.Get("target_hw") { 166 | t.Errorf("test %d, target_hw: got %v, want %v", i, actual.Get("target_hw"), v.expected.Get("target_hw")) 167 | } 168 | if actual.Get("other") != v.expected.Get("other") { 169 | t.Errorf("test %d, other: got %v, want %v", i, actual.Get("other"), v.expected.Get("other")) 170 | } 171 | if actual.Get("language") != v.expected.Get("language") { 172 | t.Errorf("test %d, language: got %v, want %v", i, actual.Get("language"), v.expected.Get("language")) 173 | } 174 | } 175 | } 176 | 177 | func TestUnbindFS(t *testing.T) { 178 | vectors := []struct { 179 | s string 180 | expected common.WellFormedName 181 | wantErr error 182 | }{{ 183 | s: "cpe:2.3:a:micr\\?osoft:internet_explorer:8.0.6001:beta:*:*:*:*:*:*", 184 | expected: common.WellFormedName{ 185 | "part": "a", 186 | "vendor": `micr\?osoft`, 187 | "product": "internet_explorer", 188 | "version": `8\.0\.6001`, 189 | "update": "beta", 190 | "edition": any, 191 | "sw_edition": any, 192 | "target_sw": any, 193 | "target_hw": any, 194 | "other": any, 195 | "language": any, 196 | }, 197 | }, { 198 | s: `cpe:2.3:a:\$0.99_kindle_books_project:\$0.99_kindle_books:6:*:*:*:*:android:*:*`, 199 | expected: common.WellFormedName{ 200 | "part": "a", 201 | "vendor": `\$0\.99_kindle_books_project`, 202 | "product": `\$0\.99_kindle_books`, 203 | "version": `6`, 204 | "update": any, 205 | "edition": any, 206 | "sw_edition": any, 207 | "target_sw": `android`, 208 | "target_hw": any, 209 | "other": any, 210 | "language": any, 211 | }, 212 | }, { 213 | s: `cpe:2.3:a:2glux*:??com_sexypolling??:0.9.1:-:-:*:-:joomla\!:*:*`, 214 | expected: common.WellFormedName{ 215 | "part": "a", 216 | "vendor": `2glux*`, 217 | "product": `??com_sexypolling??`, 218 | "version": `0\.9\.1`, 219 | "update": na, 220 | "edition": na, 221 | "sw_edition": na, 222 | "target_sw": `joomla\!`, 223 | "target_hw": any, 224 | "other": any, 225 | "language": any, 226 | }, 227 | }, { 228 | // invalid prefix 229 | s: `cpe:2.4:a:2glux:com_sexypolling:0.9.1:-:-:*:-:joomla\!:*:*`, 230 | wantErr: common.ErrParse, 231 | }, { 232 | // embedded unquoted * 233 | s: `cpe:2.3:a:2g*lux:com_sexypolling:0.9.1:-:-:*:-:joomla\!:*:*`, 234 | wantErr: common.ErrParse, 235 | }, { 236 | // embedded unquoted ? 237 | s: `cpe:2.3:a:2g?lux:com_sexypolling:0.9.1:-:-:*:-:joomla\!:*:*`, 238 | wantErr: common.ErrParse, 239 | }, { 240 | // invalid part 241 | s: `cpe:2.3:z:2glux*:??com_sexypolling??:0.9.1:-:-:*:-:joomla\!:*:*`, 242 | wantErr: common.ErrParse, 243 | }, { 244 | // empty part 245 | s: `cpe:2.3::2glux*:??com_sexypolling??:0.9.1:-:-:*:-:joomla\!:*:*`, 246 | wantErr: common.ErrParse, 247 | }, 248 | } 249 | 250 | for i, v := range vectors { 251 | actual, err := UnbindFS(v.s) 252 | if errors.Cause(err) != v.wantErr { 253 | fmt.Println(err) 254 | t.Errorf("test %d, Error: got %v, want %v", i, errors.Cause(err), v.wantErr) 255 | } 256 | if err != nil { 257 | continue 258 | } 259 | if actual.Get("part") != v.expected.Get("part") { 260 | t.Errorf("test %d, part: got %v, want %v", i, actual.Get("part"), v.expected.Get("part")) 261 | } 262 | if actual.Get("vendor") != v.expected.Get("vendor") { 263 | t.Errorf("test %d, vendor: got %v, want %v", i, actual.Get("vendor"), v.expected.Get("vendor")) 264 | } 265 | if actual.Get("product") != v.expected.Get("product") { 266 | t.Errorf("test %d, product: got %v, want %v", i, actual.Get("product"), v.expected.Get("product")) 267 | } 268 | if actual.Get("version") != v.expected.Get("version") { 269 | t.Errorf("test %d, version: got %v, want %v", i, actual.Get("version"), v.expected.Get("version")) 270 | } 271 | if actual.Get("update") != v.expected.Get("update") { 272 | t.Errorf("test %d, update: got %v, want %v", i, actual.Get("update"), v.expected.Get("update")) 273 | } 274 | if actual.Get("edition") != v.expected.Get("edition") { 275 | t.Errorf("test %d, edition: got %v, want %v", i, actual.Get("edition"), v.expected.Get("edition")) 276 | } 277 | if actual.Get("sw_edition") != v.expected.Get("sw_edition") { 278 | t.Errorf("test %d, sw_edition: got %v, want %v", i, actual.Get("sw_edition"), v.expected.Get("sw_edition")) 279 | } 280 | if actual.Get("target_sw") != v.expected.Get("target_sw") { 281 | t.Errorf("test %d, target_sw: got %v, want %v", i, actual.Get("target_sw"), v.expected.Get("target_sw")) 282 | } 283 | if actual.Get("target_hw") != v.expected.Get("target_hw") { 284 | t.Errorf("test %d, target_hw: got %v, want %v", i, actual.Get("target_hw"), v.expected.Get("target_hw")) 285 | } 286 | if actual.Get("other") != v.expected.Get("other") { 287 | t.Errorf("test %d, other: got %v, want %v", i, actual.Get("other"), v.expected.Get("other")) 288 | } 289 | if actual.Get("language") != v.expected.Get("language") { 290 | t.Errorf("test %d, language: got %v, want %v", i, actual.Get("language"), v.expected.Get("language")) 291 | } 292 | } 293 | 294 | } 295 | 296 | func TestGetCompURI(t *testing.T) { 297 | vectors := []struct { 298 | uri string 299 | index int 300 | expected string 301 | }{{ 302 | uri: "cpe:/a:microsoft:internet_explorer%01%01%01%01:?:beta", 303 | index: 0, 304 | expected: "cpe:", 305 | }, { 306 | uri: "cpe:/a:microsoft:internet_explorer%01%01%01%01:?:beta", 307 | index: 1, 308 | expected: "a", 309 | }, { 310 | uri: "cpe:/a:microsoft:internet_explorer%01%01%01%01:?:beta", 311 | index: 2, 312 | expected: "microsoft", 313 | }, { 314 | uri: "cpe:/a:microsoft:internet_explorer%01%01%01%01:?:beta", 315 | index: 100, 316 | expected: "", 317 | }, 318 | } 319 | 320 | for i, v := range vectors { 321 | actual := getCompURI(v.uri, v.index) 322 | if actual != v.expected { 323 | t.Errorf("test %d, Result: got %v, want %v", i, actual, v.expected) 324 | } 325 | } 326 | } 327 | 328 | func TestGetCompFS(t *testing.T) { 329 | vectors := []struct { 330 | fs string 331 | index int 332 | expected string 333 | }{{ 334 | fs: "cpe:2.3:a:microsoft:internet_explorer%01%01%01%01:?:beta", 335 | index: 0, 336 | expected: "cpe", 337 | }, { 338 | fs: "cpe:2.3:a:microsoft:internet_explorer%01%01%01%01:?:beta", 339 | index: 1, 340 | expected: "2.3", 341 | }, { 342 | fs: "cpe:2.3:a:microsoft:internet_explorer%01%01%01%01:?:beta", 343 | index: 2, 344 | expected: "a", 345 | }, { 346 | fs: "cpe:2.3:a:microsoft:internet_explorer%01%01%01%01:?:beta", 347 | index: 100, 348 | expected: "", 349 | }, { 350 | fs: "cpe:2.3:a:microsoft:internet_explorer%01%01%01%01:?:beta", 351 | index: -1, 352 | expected: "", 353 | }, { 354 | fs: `a\:b\:c:d\:e\:f:g`, 355 | index: 0, 356 | expected: `a\:b\:c`, 357 | }, { 358 | fs: `a\:b\:c:d\:e\:f:g`, 359 | index: 1, 360 | expected: `d\:e\:f`, 361 | }, { 362 | fs: `a\:b\:c:d\:e\:f:g`, 363 | index: 2, 364 | expected: "g", 365 | }, { 366 | fs: `a\:b\:c:d\:e\:f:g`, 367 | index: 3, 368 | expected: "", 369 | }, 370 | } 371 | 372 | for i, v := range vectors { 373 | actual := getCompFS(v.fs, v.index) 374 | if actual != v.expected { 375 | t.Errorf("test %d, Result: got %v, want %v", i, actual, v.expected) 376 | } 377 | } 378 | } 379 | 380 | func TestUnpack(t *testing.T) { 381 | vectors := []struct { 382 | s string 383 | expected common.WellFormedName 384 | wantErr error 385 | }{{ 386 | s: "~-~-~wordpress~~", 387 | expected: common.WellFormedName{ 388 | "edition": na, 389 | "sw_edition": na, 390 | "target_sw": "wordpress", 391 | "target_hw": any, 392 | "other": any, 393 | }, 394 | }, { 395 | s: "~~~~~standalone", 396 | expected: common.WellFormedName{ 397 | "other": "standalone", 398 | }, 399 | }, { 400 | s: "~-~portal~sw~x86~a~b~c", 401 | expected: common.WellFormedName{ 402 | "edition": na, 403 | "sw_edition": "portal", 404 | "target_sw": "sw", 405 | "target_hw": "x86", 406 | "other": `a\~b\~c`, 407 | }, 408 | }, 409 | } 410 | 411 | for i, v := range vectors { 412 | wfn := common.WellFormedName{} 413 | actual, err := unpack(v.s, wfn) 414 | if errors.Cause(err) != v.wantErr { 415 | fmt.Println(err) 416 | t.Errorf("test %d, Error: got %v, want %v", i, errors.Cause(err), v.wantErr) 417 | } 418 | if err != nil { 419 | continue 420 | } 421 | if actual.Get("edition") != v.expected.Get("edition") { 422 | t.Errorf("test %d, edition: got %v, want %v", i, actual.Get("edition"), v.expected.Get("edition")) 423 | } 424 | if actual.Get("sw_edition") != v.expected.Get("sw_edition") { 425 | t.Errorf("test %d, sw_edition: got %v, want %v", i, actual.Get("sw_edition"), v.expected.Get("sw_edition")) 426 | } 427 | if actual.Get("target_sw") != v.expected.Get("target_sw") { 428 | t.Errorf("test %d, target_sw: got %v, want %v", i, actual.Get("target_sw"), v.expected.Get("target_sw")) 429 | } 430 | if actual.Get("target_hw") != v.expected.Get("target_hw") { 431 | t.Errorf("test %d, target_hw: got %v, want %v", i, actual.Get("target_hw"), v.expected.Get("target_hw")) 432 | } 433 | if actual.Get("other") != v.expected.Get("other") { 434 | t.Errorf("test %d, other: got %v, want %v", i, actual.Get("other"), v.expected.Get("other")) 435 | } 436 | } 437 | 438 | } 439 | -------------------------------------------------------------------------------- /testing/README.md: -------------------------------------------------------------------------------- 1 | # testing 2 | 3 | Generate test cases from CPE Dictionary automatically. 4 | https://nvd.nist.gov/products/cpe 5 | 6 | - template 7 | - dictionary_test.tmpl 8 | - test file 9 | - dictionary_test.go -------------------------------------------------------------------------------- /testing/dictionary_test.tmpl: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/knqyf263/go-cpe/naming" 7 | "github.com/knqyf263/go-cpe/matching" 8 | ) 9 | 10 | func TestCompare(t *testing.T) { 11 | vectors := []struct { 12 | uri string 13 | fs string 14 | }{ 15 | {{ range $i, $v := .Pair }} 16 | { 17 | uri: `{{ $v.URI }}`, 18 | fs: `{{ $v.FS }}`, 19 | }, 20 | {{ end -}} 21 | } 22 | 23 | for i, v := range vectors { 24 | wfn, err := naming.UnbindURI(v.uri) 25 | if err != nil{ 26 | t.Errorf("test %d, Unexpected error: %s, URI: %s", i, err, v.uri) 27 | } 28 | wfn2, err := naming.UnbindFS(v.fs) 29 | if err != nil{ 30 | t.Errorf("test %d, Unexpected error: %s, FS: %s", i, err, v.fs) 31 | } 32 | matching.IsEqual(wfn, wfn2) 33 | matching.IsEqual(wfn2, wfn) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /testing/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "compress/gzip" 6 | "encoding/xml" 7 | "fmt" 8 | "html/template" 9 | "io/ioutil" 10 | "math/rand" 11 | "net/http" 12 | "os" 13 | ) 14 | 15 | // CpeList is CPE list 16 | type CpeList struct { 17 | CpeItems []CpeItem `xml:"cpe-item"` 18 | } 19 | 20 | // CpeItem has CPE information 21 | type CpeItem struct { 22 | Name string `xml:"name,attr"` 23 | Cpe23Item Cpe23Item `xml:"cpe23-item"` 24 | } 25 | 26 | // Cpe23Item has CPE 2.3 information 27 | type Cpe23Item struct { 28 | Name string `xml:"name,attr"` 29 | } 30 | 31 | // Pair has fs and uri 32 | type Pair struct { 33 | URI string 34 | FS string 35 | } 36 | 37 | func main() { 38 | url := "http://static.nvd.nist.gov/feeds/xml/cpe/dictionary/official-cpe-dictionary_v2.3.xml.gz" 39 | resp, err := http.Get(url) 40 | if err != nil || resp.StatusCode != 200 { 41 | fmt.Printf("HTTP error. errs: %s, url: %s", err, url) 42 | return 43 | } 44 | 45 | body, _ := ioutil.ReadAll(resp.Body) 46 | defer resp.Body.Close() 47 | 48 | b := bytes.NewBufferString(string(body)) 49 | reader, err := gzip.NewReader(b) 50 | defer reader.Close() 51 | if err != nil { 52 | fmt.Printf("Failed to decompress NVD feedfile. url: %s, err: %s", url, err) 53 | return 54 | } 55 | bytes, err := ioutil.ReadAll(reader) 56 | if err != nil { 57 | fmt.Printf("Failed to Read NVD feedfile. url: %s, err: %s", url, err) 58 | return 59 | } 60 | cpeList := CpeList{} 61 | if err = xml.Unmarshal(bytes, &cpeList); err != nil { 62 | fmt.Printf("Failed to unmarshal. url: %s, err: %s", url, err) 63 | return 64 | } 65 | 66 | var uriList, fsList []string 67 | for _, cpeItem := range cpeList.CpeItems { 68 | uriList = append(uriList, cpeItem.Name) 69 | fsList = append(fsList, cpeItem.Cpe23Item.Name) 70 | } 71 | shuffle(fsList) 72 | 73 | pair := []Pair{} 74 | for i, uri := range uriList { 75 | pair = append(pair, Pair{ 76 | URI: uri, 77 | FS: fsList[i], 78 | }) 79 | } 80 | fmt.Printf("%d data...\n", len(cpeList.CpeItems)) 81 | 82 | fmt.Println("Generating test code...") 83 | t := template.Must(template.ParseFiles("dictionary_test.tmpl")) 84 | file, _ := os.Create(`./dictionary_test.go`) 85 | defer file.Close() 86 | t.Execute(file, map[string]interface{}{ 87 | "Pair": pair, 88 | }) 89 | } 90 | 91 | func shuffle(data []string) { 92 | n := len(data) 93 | for i := n - 1; i >= 0; i-- { 94 | j := rand.Intn(i + 1) 95 | data[i], data[j] = data[j], data[i] 96 | } 97 | } 98 | --------------------------------------------------------------------------------