├── .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 | [](https://travis-ci.org/knqyf263/go-cpe)
4 | [](https://coveralls.io/github/knqyf263/go-cpe?branch=initial)
5 | [](https://goreportcard.com/report/github.com/knqyf263/go-cpe)
6 | [](https://godoc.org/github.com/knqyf263/go-cpe)
7 | [](/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 |
--------------------------------------------------------------------------------