├── .dockerignore ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── FEATURE_REQUEST.md │ ├── SUPPORT_QUESTION.md │ └── BUG_REPORT.md ├── workflows │ ├── test.yml │ ├── goreleaser.yml │ ├── golangci.yml │ └── docker-build.yml ├── dependabot.yml └── PULL_REQUEST_TEMPLATE.md ├── util └── util.go ├── models ├── alpine │ ├── types.go │ └── alpine.go ├── models_test.go ├── util │ ├── util.go │ └── util_test.go ├── amazon │ ├── types.go │ └── amazon.go ├── fedora │ ├── types.go │ └── fedora.go ├── debian │ ├── debian.go │ ├── debian_test.go │ └── types.go ├── oracle │ ├── oracle.go │ └── types.go ├── models.go ├── suse │ └── types.go ├── redhat │ ├── redhat.go │ ├── redhat_test.go │ └── types.go └── ubuntu │ ├── ubuntu_test.go │ ├── types.go │ └── ubuntu.go ├── commands ├── version.go ├── fetch.go ├── server.go ├── fetch-fedora.go ├── root.go ├── fetch-amazon.go ├── fetch-alpine.go ├── fetch-ubuntu.go ├── fetch-debian.go ├── fetch-oracle.go ├── fetch-redhat.go ├── fetch-suse.go └── select.go ├── main.go ├── .gitignore ├── fetcher ├── util │ ├── util.go │ ├── util_test.go │ └── fetcher.go ├── oracle │ └── oracle.go ├── amazon │ ├── types.go │ └── amazon.go ├── alpine │ └── alpine.go ├── suse │ └── suse.go ├── debian │ └── debian.go ├── ubuntu │ └── ubuntu.go ├── fedora │ ├── types.go │ └── types_test.go └── redhat │ └── redhat.go ├── .revive.toml ├── .goreleaser.yml ├── Dockerfile ├── GNUmakefile ├── log └── log.go ├── .golangci.yml ├── config └── config.go ├── go.mod ├── db ├── db.go └── db_test.go └── server └── server.go /.dockerignore: -------------------------------------------------------------------------------- 1 | .dockerignore 2 | Dockerfile 3 | vendor/ 4 | cve.sqlite3 5 | oval.sqlite3* 6 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: kotakanbe 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | labels: enhancement 4 | about: I have a suggestion (and might want to implement myself)! 5 | --- 6 | 7 | 10 | -------------------------------------------------------------------------------- /util/util.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "maps" 5 | "slices" 6 | ) 7 | 8 | // Unique return unique elements 9 | func Unique[T comparable](s []T) []T { 10 | m := map[T]struct{}{} 11 | for _, v := range s { 12 | m[v] = struct{}{} 13 | } 14 | return slices.Collect(maps.Keys(m)) 15 | } 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/SUPPORT_QUESTION.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Support Question 3 | labels: question 4 | about: If you have a question about Vuls. 5 | --- 6 | 7 | 11 | -------------------------------------------------------------------------------- /models/alpine/types.go: -------------------------------------------------------------------------------- 1 | package alpine 2 | 3 | // SecDB is a struct of alpine secdb 4 | type SecDB struct { 5 | Distroversion string 6 | Reponame string 7 | Urlprefix string 8 | Apkurl string 9 | Packages []struct { 10 | Pkg struct { 11 | Name string 12 | Secfixes map[string][]string 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | build: 7 | name: Build 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Check out code into the Go module directory 11 | uses: actions/checkout@v6 12 | - name: Set up Go 1.x 13 | uses: actions/setup-go@v6 14 | with: 15 | go-version-file: go.mod 16 | - name: Test 17 | run: make test 18 | -------------------------------------------------------------------------------- /commands/version.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | 8 | "github.com/vulsio/goval-dictionary/config" 9 | ) 10 | 11 | func init() { 12 | RootCmd.AddCommand(versionCmd) 13 | } 14 | 15 | var versionCmd = &cobra.Command{ 16 | Use: "version", 17 | Short: "Show version", 18 | Long: `Show version`, 19 | Run: func(_ *cobra.Command, _ []string) { 20 | fmt.Printf("goval-dictionary %s %s\n", config.Version, config.Revision) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | "github.com/vulsio/goval-dictionary/commands" 9 | ) 10 | 11 | // Name ... Name 12 | const Name string = "goval-dictionary" 13 | 14 | func main() { 15 | if envArgs := os.Getenv("GOVAL_DICTIONARY_ARGS"); 0 < len(envArgs) { 16 | commands.RootCmd.SetArgs(strings.Fields(envArgs)) 17 | } 18 | 19 | if err := commands.RootCmd.Execute(); err != nil { 20 | fmt.Fprintln(os.Stderr, err) 21 | os.Exit(1) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | .vscode 27 | coverage.out 28 | vendor/ 29 | goval-dictionary 30 | *.sqlite3 31 | *.sqlite3-shm 32 | *.sqlite3-wal 33 | *.sqlite3-journal 34 | tags 35 | /dist/ 36 | -------------------------------------------------------------------------------- /fetcher/util/util.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "regexp" 4 | 5 | // CveIDPattern is regexp matches to `CVE-\d{4}-\d{4,}` 6 | var CveIDPattern = regexp.MustCompile(`CVE-\d{4}-\d{4,}`) 7 | 8 | // UniqueStrings eliminates duplication from []string 9 | func UniqueStrings(s []string) []string { 10 | if len(s) == 0 { 11 | return nil 12 | } 13 | m := make(map[string]struct{}, len(s)) 14 | for _, v := range s { 15 | m[v] = struct{}{} 16 | } 17 | uniq := make([]string, 0, len(m)) 18 | for v := range m { 19 | uniq = append(uniq, v) 20 | } 21 | return uniq 22 | } 23 | -------------------------------------------------------------------------------- /models/models_test.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func Test_FetchMeta(t *testing.T) { 8 | var tests = []struct { 9 | in FetchMeta 10 | outdated bool 11 | }{ 12 | { 13 | in: FetchMeta{ 14 | SchemaVersion: 1, 15 | }, 16 | outdated: true, 17 | }, 18 | { 19 | in: FetchMeta{ 20 | SchemaVersion: LatestSchemaVersion, 21 | }, 22 | outdated: false, 23 | }, 24 | } 25 | 26 | for i, tt := range tests { 27 | if aout := tt.in.OutDated(); tt.outdated != aout { 28 | t.Errorf("[%d] outdated expected: %#v\n actual: %#v\n", i, tt.outdated, aout) 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /models/util/util.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/inconshreveable/log15" 7 | ) 8 | 9 | // ParsedOrDefaultTime returns time.Parse(layout, value), or time.Date(1000, time.January, 1, 0, 0, 0, 0, time.UTC) if it failed to parse 10 | func ParsedOrDefaultTime(layouts []string, value string) time.Time { 11 | defaultTime := time.Date(1000, time.January, 1, 0, 0, 0, 0, time.UTC) 12 | if value == "" || value == "unknown" { 13 | return defaultTime 14 | } 15 | 16 | for _, layout := range layouts { 17 | if t, err := time.Parse(layout, value); err == nil { 18 | return t 19 | } 20 | } 21 | log15.Warn("Failed to parse string", "timeformat", layouts, "target string", value) 22 | return defaultTime 23 | } 24 | -------------------------------------------------------------------------------- /.revive.toml: -------------------------------------------------------------------------------- 1 | ignoreGeneratedHeader = false 2 | severity = "warning" 3 | confidence = 0.8 4 | errorCode = 0 5 | warningCode = 0 6 | 7 | [rule.blank-imports] 8 | [rule.context-as-argument] 9 | [rule.context-keys-type] 10 | [rule.dot-imports] 11 | [rule.error-return] 12 | [rule.error-strings] 13 | [rule.error-naming] 14 | [rule.exported] 15 | [rule.if-return] 16 | [rule.increment-decrement] 17 | [rule.var-naming] 18 | [rule.var-declaration] 19 | [rule.package-comments] 20 | [rule.range] 21 | [rule.receiver-naming] 22 | [rule.time-naming] 23 | [rule.unexported-return] 24 | [rule.indent-error-flow] 25 | [rule.errorf] 26 | [rule.empty-block] 27 | [rule.superfluous-else] 28 | [rule.unused-parameter] 29 | [rule.unreachable-code] 30 | [rule.redefines-builtin-id] -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | project_name: goval-dictionary 2 | release: 3 | github: 4 | owner: vulsio 5 | name: goval-dictionary 6 | env: 7 | - CGO_ENABLED=0 8 | builds: 9 | - id: goval-dictionary 10 | goos: 11 | - linux 12 | - windows 13 | - darwin 14 | goarch: 15 | - amd64 16 | - arm64 17 | main: . 18 | ldflags: -s -w -X github.com/vulsio/goval-dictionary/config.Version={{.Version}} -X github.com/vulsio/goval-dictionary/config.Revision={{.Commit}} 19 | binary: goval-dictionary 20 | archives: 21 | - name_template: '{{ .Binary }}_{{.Version}}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}' 22 | format: tar.gz 23 | files: 24 | - LICENSE 25 | - README* 26 | snapshot: 27 | name_template: SNAPSHOT-{{ .Commit }} 28 | -------------------------------------------------------------------------------- /commands/fetch.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | "github.com/spf13/viper" 6 | ) 7 | 8 | // fetchCmd represents the fetch command 9 | var fetchCmd = &cobra.Command{ 10 | Use: "fetch", 11 | Short: "Fetch Vulnerability dictionary", 12 | Long: `Fetch Vulnerability dictionary`, 13 | } 14 | 15 | func init() { 16 | RootCmd.AddCommand(fetchCmd) 17 | 18 | fetchCmd.PersistentFlags().Bool("no-details", false, "without vulnerability details") 19 | _ = viper.BindPFlag("no-details", fetchCmd.PersistentFlags().Lookup("no-details")) 20 | 21 | fetchCmd.PersistentFlags().Int("batch-size", 25, "The number of batch size to insert.") 22 | _ = viper.BindPFlag("batch-size", fetchCmd.PersistentFlags().Lookup("batch-size")) 23 | } 24 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:alpine as builder 2 | 3 | RUN apk add --no-cache \ 4 | git \ 5 | make \ 6 | gcc \ 7 | musl-dev 8 | 9 | ENV REPOSITORY github.com/vulsio/goval-dictionary 10 | COPY . $GOPATH/src/$REPOSITORY 11 | RUN cd $GOPATH/src/$REPOSITORY && make install 12 | 13 | 14 | FROM alpine:3.22 15 | 16 | LABEL maintainer sadayuki-matsuno 17 | 18 | ENV LOGDIR /var/log/goval-dictionary 19 | ENV WORKDIR /goval-dictionary 20 | 21 | RUN apk add --no-cache ca-certificates \ 22 | && mkdir -p $WORKDIR $LOGDIR 23 | 24 | COPY --from=builder /go/bin/goval-dictionary /usr/local/bin/ 25 | 26 | VOLUME ["$WORKDIR", "$LOGDIR"] 27 | WORKDIR $WORKDIR 28 | ENV PWD $WORKDIR 29 | 30 | ENTRYPOINT ["goval-dictionary"] 31 | CMD ["--help"] 32 | -------------------------------------------------------------------------------- /.github/workflows/goreleaser.yml: -------------------------------------------------------------------------------- 1 | name: goreleaser 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | goreleaser: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - 13 | name: Checkout 14 | uses: actions/checkout@v6 15 | - 16 | name: Unshallow 17 | run: git fetch --prune --unshallow 18 | - 19 | name: Set up Go 20 | uses: actions/setup-go@v6 21 | with: 22 | go-version-file: go.mod 23 | - 24 | name: Run GoReleaser 25 | uses: goreleaser/goreleaser-action@v6 26 | with: 27 | distribution: goreleaser 28 | version: latest 29 | args: release --clean 30 | env: 31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 32 | -------------------------------------------------------------------------------- /fetcher/oracle/oracle.go: -------------------------------------------------------------------------------- 1 | package oracle 2 | 3 | import ( 4 | "golang.org/x/xerrors" 5 | 6 | "github.com/vulsio/goval-dictionary/fetcher/util" 7 | ) 8 | 9 | func newFetchRequests() (reqs []util.FetchRequest) { 10 | const t = "https://linux.oracle.com/security/oval/com.oracle.elsa-all.xml.bz2" 11 | reqs = append(reqs, util.FetchRequest{ 12 | URL: t, 13 | MIMEType: util.MIMETypeBzip2, 14 | }) 15 | return 16 | } 17 | 18 | // FetchFiles fetch OVAL from Oracle 19 | func FetchFiles() ([]util.FetchResult, error) { 20 | reqs := newFetchRequests() 21 | if len(reqs) == 0 { 22 | return nil, xerrors.New("There are no versions to fetch") 23 | } 24 | results, err := util.FetchFeedFiles(reqs) 25 | if err != nil { 26 | return nil, xerrors.Errorf("Failed to fetch. err: %w", err) 27 | } 28 | return results, nil 29 | } 30 | -------------------------------------------------------------------------------- /fetcher/amazon/types.go: -------------------------------------------------------------------------------- 1 | package amazon 2 | 3 | // extrasCatalog is a struct of extras-catalog.json for Amazon Linux 2 Extra Repository 4 | type extrasCatalog struct { 5 | Topics []struct { 6 | N string `json:"n"` 7 | Inst []string `json:"inst,omitempty"` 8 | Versions []string `json:"versions"` 9 | DeprecatedAt string `json:"deprecated-at,omitempty"` 10 | Visible []string `json:"visible,omitempty"` 11 | } `json:"topics"` 12 | } 13 | 14 | // repoMd has repomd data 15 | type repoMd struct { 16 | RepoList []repo `xml:"data"` 17 | } 18 | 19 | // repo has a repo data 20 | type repo struct { 21 | Type string `xml:"type,attr"` 22 | Location location `xml:"location"` 23 | } 24 | 25 | // location has a location of repomd 26 | type location struct { 27 | Href string `xml:"href,attr"` 28 | } 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/BUG_REPORT.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | labels: bug 4 | about: If something isn't working as expected. 5 | --- 6 | 7 | # What did you do? (required. The issue will be **closed** when not provided.) 8 | 9 | 10 | # What did you expect to happen? 11 | 12 | 13 | # What happened instead? 14 | 15 | * Current Output 16 | 17 | Please re-run the command using ```-debug``` and provide the output below. 18 | 19 | # Steps to reproduce the behaviour 20 | 21 | 22 | # Configuration (**MUST** fill this out): 23 | 24 | * Go version (`go version`): 25 | 26 | * Go environment (`go env`): 27 | 28 | * goval-dictionary environment: 29 | 30 | Hash : ____ 31 | 32 | To check the commit hash of HEAD 33 | $ goval-dictionary -v 34 | 35 | or 36 | 37 | $ cd $GOPATH/src/github.com/vulsio/goval-dictionary 38 | $ git rev-parse --short HEAD 39 | 40 | * command: 41 | 42 | -------------------------------------------------------------------------------- /.github/workflows/golangci.yml: -------------------------------------------------------------------------------- 1 | name: golangci-lint 2 | on: 3 | push: 4 | tags: 5 | - v* 6 | branches: 7 | - master 8 | pull_request: 9 | jobs: 10 | golangci: 11 | name: lint 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Check out code into the Go module directory 15 | uses: actions/checkout@v6 16 | - name: Set up Go 1.x 17 | uses: actions/setup-go@v6 18 | with: 19 | go-version-file: go.mod 20 | - name: golangci-lint 21 | uses: golangci/golangci-lint-action@v9 22 | with: 23 | # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version 24 | version: latest 25 | 26 | # Optional: working directory, useful for monorepos 27 | # working-directory: somedir 28 | 29 | # Optional: golangci-lint command line arguments. 30 | # args: --issues-exit-code=0 31 | 32 | # Optional: show only new issues if it's a pull request. The default value is `false`. 33 | # only-new-issues: true 34 | -------------------------------------------------------------------------------- /fetcher/util/util_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "sort" 5 | "testing" 6 | 7 | "github.com/google/go-cmp/cmp" 8 | ) 9 | 10 | func TestCveIDPattern(t *testing.T) { 11 | tests := []struct { 12 | name string 13 | id string 14 | want bool 15 | }{ 16 | { 17 | name: "normal", 18 | id: "CVE-2022-0001", 19 | want: true, 20 | }, 21 | { 22 | name: "ID_with_5_digits", 23 | id: "CVE-2022-00001", 24 | want: true, 25 | }, 26 | { 27 | name: "invalid_cve_id", 28 | id: "CVE-01-0001", 29 | want: false, 30 | }, 31 | } 32 | for _, tt := range tests { 33 | t.Run(tt.name, func(t *testing.T) { 34 | got := CveIDPattern.Match([]byte(tt.id)) 35 | if got != tt.want { 36 | t.Errorf("got = %v, want = %v", got, tt.want) 37 | } 38 | }) 39 | } 40 | } 41 | 42 | func TestUniqueStrings(t *testing.T) { 43 | in := []string{"1", "1", "2", "3", "1", "2"} 44 | got := UniqueStrings(in) 45 | sort.Slice(got, func(i, j int) bool { return got[i] < got[j] }) 46 | want := []string{"1", "2", "3"} 47 | if diff := cmp.Diff(got, want); diff != "" { 48 | t.Errorf("(-got +want):\n%s", diff) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /fetcher/alpine/alpine.go: -------------------------------------------------------------------------------- 1 | package alpine 2 | 3 | import ( 4 | "fmt" 5 | 6 | "golang.org/x/xerrors" 7 | 8 | "github.com/vulsio/goval-dictionary/fetcher/util" 9 | ) 10 | 11 | const community = "https://secdb.alpinelinux.org/v%s/community.yaml" 12 | const main = "https://secdb.alpinelinux.org/v%s/main.yaml" 13 | 14 | func newFetchRequests(target []string) (reqs []util.FetchRequest) { 15 | for _, v := range target { 16 | reqs = append(reqs, util.FetchRequest{ 17 | Target: v, 18 | URL: fmt.Sprintf(main, v), 19 | MIMEType: util.MIMETypeYml, 20 | }) 21 | 22 | if v != "3.2" { 23 | reqs = append(reqs, util.FetchRequest{ 24 | Target: v, 25 | URL: fmt.Sprintf(community, v), 26 | MIMEType: util.MIMETypeYml, 27 | }) 28 | } 29 | } 30 | return 31 | } 32 | 33 | // FetchFiles fetch from alpine secdb 34 | // https://secdb.alpinelinux.org/ 35 | func FetchFiles(versions []string) ([]util.FetchResult, error) { 36 | reqs := newFetchRequests(versions) 37 | if len(reqs) == 0 { 38 | return nil, xerrors.New("There are no versions to fetch") 39 | } 40 | results, err := util.FetchFeedFiles(reqs) 41 | if err != nil { 42 | return nil, xerrors.Errorf("Failed to fetch. err: %w", err) 43 | } 44 | 45 | return results, nil 46 | } 47 | -------------------------------------------------------------------------------- /.github/workflows/docker-build.yml: -------------------------------------------------------------------------------- 1 | name: Publish Docker image 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'master' 7 | tags: 8 | - '*' 9 | 10 | jobs: 11 | docker: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v6 16 | 17 | - name: Set up QEMU 18 | uses: docker/setup-qemu-action@v3 19 | 20 | - name: Set up Docker Buildx 21 | uses: docker/setup-buildx-action@v3 22 | 23 | - name: Login to DockerHub 24 | uses: docker/login-action@v3 25 | with: 26 | username: ${{ secrets.DOCKERHUB_USERNAME }} 27 | password: ${{ secrets.DOCKERHUB_TOKEN }} 28 | 29 | - 30 | name: Docker meta 31 | id: meta 32 | uses: docker/metadata-action@v5 33 | with: 34 | images: vuls/goval-dictionary 35 | tags: | 36 | type=ref,event=tag 37 | 38 | - name: Build and push 39 | uses: docker/build-push-action@v6 40 | with: 41 | push: true 42 | context: . 43 | tags: | 44 | vuls/goval-dictionary:latest 45 | ${{ steps.meta.outputs.tags }} 46 | secrets: | 47 | "github_token=${{ secrets.GITHUB_TOKEN }}" 48 | platforms: linux/amd64,linux/arm64 49 | -------------------------------------------------------------------------------- /fetcher/suse/suse.go: -------------------------------------------------------------------------------- 1 | package suse 2 | 3 | import ( 4 | "fmt" 5 | 6 | "golang.org/x/xerrors" 7 | 8 | "github.com/vulsio/goval-dictionary/fetcher/util" 9 | ) 10 | 11 | // https://ftp.suse.com/pub/projects/security/oval/opensuse.leap.42.2.xml.gz 12 | // https://ftp.suse.com/pub/projects/security/oval/opensuse.13.2.xml.gz 13 | // https://ftp.suse.com/pub/projects/security/oval/suse.linux.enterprise.desktop.12.xml.gz 14 | // https://ftp.suse.com/pub/projects/security/oval/suse.linux.enterprise.server.12.xml.gz 15 | func newFetchRequests(suseType string, target []string) (reqs []util.FetchRequest) { 16 | const t = "https://ftp.suse.com/pub/projects/security/oval/%s.%s.xml.gz" 17 | for _, v := range target { 18 | reqs = append(reqs, util.FetchRequest{ 19 | Target: v, 20 | URL: fmt.Sprintf(t, suseType, v), 21 | MIMEType: util.MIMETypeGzip, 22 | }) 23 | } 24 | return 25 | } 26 | 27 | // FetchFiles fetch OVAL from SUSE 28 | func FetchFiles(suseType string, versions []string) ([]util.FetchResult, error) { 29 | reqs := newFetchRequests(suseType, versions) 30 | if len(reqs) == 0 { 31 | return nil, xerrors.New("There are no versions to fetch") 32 | } 33 | results, err := util.FetchFeedFiles(reqs) 34 | if err != nil { 35 | return nil, xerrors.Errorf("Failed to fetch. err: %w", err) 36 | } 37 | return results, nil 38 | } 39 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "gomod" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | target-branch: "master" 13 | ignore: 14 | - dependency-name: "gorm.io/driver/mysql" 15 | - dependency-name: "gorm.io/driver/postgres" 16 | - dependency-name: "gorm.io/gorm" 17 | groups: 18 | all: 19 | patterns: 20 | - "*" 21 | exclude-patterns: 22 | - github.com/glebarez/sqlite 23 | - package-ecosystem: "github-actions" # See documentation for possible values 24 | directory: "/" # Location of package manifests 25 | schedule: 26 | interval: "weekly" 27 | target-branch: "master" 28 | groups: 29 | all: 30 | patterns: 31 | - "*" 32 | - package-ecosystem: "docker" 33 | directory: "/" 34 | schedule: 35 | interval: "weekly" 36 | target-branch: "master" 37 | groups: 38 | all: 39 | patterns: 40 | - "*" 41 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | If this Pull Request is work in progress, Add a prefix of “[WIP]” in the title. 3 | 4 | # What did you implement: 5 | 6 | Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. 7 | 8 | Fixes # (issue) 9 | 10 | ## Type of change 11 | 12 | Please delete options that are not relevant. 13 | 14 | - [ ] Bug fix (non-breaking change which fixes an issue) 15 | - [ ] New feature (non-breaking change which adds functionality) 16 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 17 | - [ ] This change requires a documentation update 18 | 19 | # How Has This Been Tested? 20 | 21 | Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. 22 | 23 | # Checklist: 24 | You don't have to satisfy all of the following. 25 | 26 | - [ ] Write tests 27 | - [ ] Write documentation 28 | - [ ] Check that there aren't other open pull requests for the same issue/feature 29 | - [ ] Format your source code by `make fmt` 30 | - [ ] Pass the test by `make test` 31 | - [ ] Provide verification config / commands 32 | - [ ] Enable "Allow edits from maintainers" for this PR 33 | - [ ] Update the messages below 34 | 35 | ***Is this ready for review?:*** NO 36 | 37 | # Reference 38 | 39 | * https://blog.github.com/2015-01-21-how-to-write-the-perfect-pull-request/ 40 | 41 | -------------------------------------------------------------------------------- /models/util/util_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestParsedOrDefaultTime(t *testing.T) { 9 | tests := []struct { 10 | name string 11 | in string 12 | layouts []string 13 | want time.Time 14 | }{ 15 | { 16 | name: "success to parse", 17 | in: "2021-01-02", 18 | layouts: []string{"2006-01-02"}, 19 | want: time.Date(2021, time.January, 2, 0, 0, 0, 0, time.UTC), 20 | }, 21 | { 22 | name: "success to parse(multi layout)", 23 | in: "2021-01-02 15:00:00", 24 | layouts: []string{"2006-01-02", "2006-01-02 15:04:05"}, 25 | want: time.Date(2021, time.January, 2, 15, 0, 0, 0, time.UTC), 26 | }, 27 | { 28 | name: "failed to parse", 29 | in: "2021/01/02", 30 | layouts: []string{"2006-01-02"}, 31 | want: time.Date(1000, time.January, 1, 0, 0, 0, 0, time.UTC), 32 | }, 33 | { 34 | name: "empty string", 35 | in: "", 36 | layouts: []string{"2006-01-02"}, 37 | want: time.Date(1000, time.January, 1, 0, 0, 0, 0, time.UTC), 38 | }, 39 | { 40 | name: "unknown", 41 | in: "unknown", 42 | layouts: []string{"2006-01-02"}, 43 | want: time.Date(1000, time.January, 1, 0, 0, 0, 0, time.UTC), 44 | }, 45 | } 46 | for _, tt := range tests { 47 | t.Run(tt.name, func(t *testing.T) { 48 | if got := ParsedOrDefaultTime(tt.layouts, tt.in); got != tt.want { 49 | t.Errorf("got: %v, want: %v", got, tt.want) 50 | } 51 | }) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /GNUmakefile: -------------------------------------------------------------------------------- 1 | .PHONY: \ 2 | all \ 3 | build \ 4 | install \ 5 | lint \ 6 | golangci \ 7 | vet \ 8 | fmt \ 9 | mlint \ 10 | fmtcheck \ 11 | pretest \ 12 | test \ 13 | unused \ 14 | cov \ 15 | clean 16 | 17 | SRCS = $(shell git ls-files '*.go') 18 | PKGS = $(shell go list ./...) 19 | VERSION := $(shell git describe --tags --abbrev=0) 20 | REVISION := $(shell git rev-parse --short HEAD) 21 | LDFLAGS := -X 'github.com/vulsio/goval-dictionary/config.Version=$(VERSION)' \ 22 | -X 'github.com/vulsio/goval-dictionary/config.Revision=$(REVISION)' 23 | GO := CGO_ENABLED=0 go 24 | 25 | all: build test 26 | 27 | build: main.go 28 | $(GO) build -a -ldflags "$(LDFLAGS)" -o goval-dictionary $< 29 | 30 | install: main.go 31 | $(GO) install -ldflags "$(LDFLAGS)" 32 | 33 | lint: 34 | go install github.com/mgechev/revive@latest 35 | revive -config ./.revive.toml -formatter plain $(PKGS) 36 | 37 | golangci: 38 | go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest 39 | golangci-lint run 40 | 41 | vet: 42 | echo $(PKGS) | xargs env $(GO) vet || exit; 43 | 44 | fmt: 45 | gofmt -s -w $(SRCS) 46 | 47 | fmtcheck: 48 | $(foreach file,$(SRCS),gofmt -s -d $(file);) 49 | 50 | pretest: lint vet fmtcheck 51 | 52 | test: pretest 53 | $(GO) test -cover -v ./... || exit; 54 | 55 | cov: 56 | @ go get -v github.com/axw/gocov/gocov 57 | @ go get golang.org/x/tools/cmd/cover 58 | gocov test | gocov report 59 | 60 | clean: 61 | echo $(PKGS) | xargs go clean || exit; 62 | echo $(PKGS) | xargs go clean || exit; 63 | -------------------------------------------------------------------------------- /fetcher/debian/debian.go: -------------------------------------------------------------------------------- 1 | package debian 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/inconshreveable/log15" 7 | "golang.org/x/xerrors" 8 | 9 | "github.com/vulsio/goval-dictionary/config" 10 | "github.com/vulsio/goval-dictionary/fetcher/util" 11 | ) 12 | 13 | // https://www.debian.org/security/oval/ 14 | func newFetchRequests(target []string) (reqs []util.FetchRequest) { 15 | const t = "https://www.debian.org/security/oval/oval-definitions-%s.xml.bz2" 16 | for _, v := range target { 17 | var name string 18 | if name = debianName(v); name == "unknown" { 19 | log15.Warn("Skip unknown debian.", "version", v) 20 | continue 21 | } 22 | reqs = append(reqs, util.FetchRequest{ 23 | Target: v, 24 | URL: fmt.Sprintf(t, name), 25 | MIMEType: util.MIMETypeBzip2, 26 | }) 27 | } 28 | return 29 | } 30 | 31 | func debianName(major string) string { 32 | switch major { 33 | case "7": 34 | return config.Debian7 35 | case "8": 36 | return config.Debian8 37 | case "9": 38 | return config.Debian9 39 | case "10": 40 | return config.Debian10 41 | case "11": 42 | return config.Debian11 43 | case "12": 44 | return config.Debian12 45 | case "13": 46 | return config.Debian13 47 | default: 48 | return "unknown" 49 | } 50 | } 51 | 52 | // FetchFiles fetch OVAL from Debian 53 | func FetchFiles(versions []string) ([]util.FetchResult, error) { 54 | reqs := newFetchRequests(versions) 55 | if len(reqs) == 0 { 56 | return nil, xerrors.New("There are no versions to fetch") 57 | } 58 | results, err := util.FetchFeedFiles(reqs) 59 | if err != nil { 60 | return nil, xerrors.Errorf("Failed to fetch. err: %w", err) 61 | } 62 | return results, nil 63 | } 64 | -------------------------------------------------------------------------------- /log/log.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "runtime" 7 | 8 | "github.com/inconshreveable/log15" 9 | "golang.org/x/xerrors" 10 | ) 11 | 12 | // GetDefaultLogDir returns default log directory 13 | func GetDefaultLogDir() string { 14 | defaultLogDir := "/var/log/goval-dictionary" 15 | if runtime.GOOS == "windows" { 16 | defaultLogDir = filepath.Join(os.Getenv("APPDATA"), "goval-dictionary") 17 | } 18 | return defaultLogDir 19 | } 20 | 21 | // SetLogger set logger 22 | func SetLogger(logToFile bool, logDir string, debug, logJSON bool) error { 23 | stderrHandler := log15.StderrHandler 24 | logFormat := log15.LogfmtFormat() 25 | if logJSON { 26 | logFormat = log15.JsonFormatEx(false, true) 27 | stderrHandler = log15.StreamHandler(os.Stderr, logFormat) 28 | } 29 | 30 | lvlHandler := log15.LvlFilterHandler(log15.LvlInfo, stderrHandler) 31 | if debug { 32 | lvlHandler = log15.LvlFilterHandler(log15.LvlDebug, stderrHandler) 33 | } 34 | 35 | var handler log15.Handler 36 | if logToFile { 37 | if _, err := os.Stat(logDir); err != nil { 38 | if os.IsNotExist(err) { 39 | if err := os.Mkdir(logDir, 0700); err != nil { 40 | return xerrors.Errorf("Failed to create log directory. err: %w", err) 41 | } 42 | } else { 43 | return xerrors.Errorf("Failed to check log directory. err: %w", err) 44 | } 45 | } 46 | 47 | logPath := filepath.Join(logDir, "goval-dictionary.log") 48 | if _, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644); err != nil { 49 | return xerrors.Errorf("Failed to open a log file. err: %w", err) 50 | } 51 | handler = log15.MultiHandler( 52 | log15.Must.FileHandler(logPath, logFormat), 53 | lvlHandler, 54 | ) 55 | } else { 56 | handler = lvlHandler 57 | } 58 | log15.Root().SetHandler(handler) 59 | return nil 60 | } 61 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | 3 | linters: 4 | default: none 5 | enable: 6 | - errcheck 7 | - govet 8 | - ineffassign 9 | - misspell 10 | - prealloc 11 | - revive 12 | - staticcheck 13 | settings: 14 | misspell: 15 | ignore-rules: 16 | - criterias 17 | revive: # https://golangci-lint.run/usage/linters/#revive 18 | rules: 19 | - name: blank-imports 20 | - name: context-as-argument 21 | - name: context-keys-type 22 | - name: dot-imports 23 | - name: empty-block 24 | - name: error-naming 25 | - name: error-return 26 | - name: error-strings 27 | - name: errorf 28 | - name: exported 29 | - name: if-return 30 | - name: increment-decrement 31 | - name: indent-error-flow 32 | - name: package-comments 33 | disabled: true 34 | - name: range 35 | - name: receiver-naming 36 | - name: redefines-builtin-id 37 | - name: superfluous-else 38 | - name: time-naming 39 | - name: unexported-return 40 | - name: unreachable-code 41 | - name: unused-parameter 42 | - name: var-declaration 43 | - name: var-naming 44 | arguments: 45 | - [] # AllowList 46 | - [] # DenyList 47 | - - skip-package-name-checks: true 48 | staticcheck: # https://golangci-lint.run/usage/linters/#staticcheck 49 | checks: 50 | - all 51 | - -ST1000 # at least one file in a package should have a package comment 52 | - -ST1005 # error strings should not be capitalized 53 | exclusions: 54 | rules: 55 | - source: "defer .+\\.Close\\(\\)" 56 | linters: 57 | - errcheck 58 | 59 | formatters: 60 | enable: 61 | - goimports 62 | 63 | run: 64 | timeout: 10m 65 | -------------------------------------------------------------------------------- /models/amazon/types.go: -------------------------------------------------------------------------------- 1 | package amazon 2 | 3 | // Reference has reference information 4 | type Reference struct { 5 | Href string `xml:"href,attr" json:"href,omitempty"` 6 | ID string `xml:"id,attr" json:"id,omitempty"` 7 | Title string `xml:"title,attr" json:"title,omitempty"` 8 | Type string `xml:"type,attr" json:"type,omitempty"` 9 | } 10 | 11 | // Package has affected package information 12 | type Package struct { 13 | Name string `xml:"name,attr" json:"name,omitempty"` 14 | Epoch string `xml:"epoch,attr" json:"epoch,omitempty"` 15 | Version string `xml:"version,attr" json:"version,omitempty"` 16 | Release string `xml:"release,attr" json:"release,omitempty"` 17 | Arch string `xml:"arch,attr" json:"arch,omitempty"` 18 | Filename string `xml:"filename" json:"filename,omitempty"` 19 | } 20 | 21 | // Updated has updated at 22 | type Updated struct { 23 | Date string `xml:"date,attr" json:"date,omitempty"` 24 | } 25 | 26 | // Issued has issued at 27 | type Issued struct { 28 | Date string `xml:"date,attr" json:"date,omitempty"` 29 | } 30 | 31 | // UpdateInfo has detailed data of Updates 32 | type UpdateInfo struct { 33 | ID string `xml:"id" json:"id,omitempty"` 34 | Title string `xml:"title" json:"title,omitempty"` 35 | Issued Issued `xml:"issued" json:"issued,omitempty"` 36 | Updated Updated `xml:"updated" json:"updated,omitempty"` 37 | Severity string `xml:"severity" json:"severity,omitempty"` 38 | Description string `xml:"description" json:"description,omitempty"` 39 | Packages []Package `xml:"pkglist>collection>package" json:"packages,omitempty"` 40 | References []Reference `xml:"references>reference" json:"references,omitempty"` 41 | CVEIDs []string `json:"cveiDs,omitempty"` 42 | Repository string `json:"repository,omitempty"` 43 | } 44 | 45 | // Updates has a list of ALAS 46 | type Updates struct { 47 | UpdateList []UpdateInfo `xml:"update"` 48 | } 49 | -------------------------------------------------------------------------------- /models/fedora/types.go: -------------------------------------------------------------------------------- 1 | package fedora 2 | 3 | // Reference has reference information 4 | type Reference struct { 5 | Href string `xml:"href,attr" json:"href,omitempty"` 6 | ID string `xml:"id,attr" json:"id,omitempty"` 7 | Title string `xml:"title,attr" json:"title,omitempty"` 8 | Type string `xml:"type,attr" json:"type,omitempty"` 9 | } 10 | 11 | // Package has affected package information 12 | type Package struct { 13 | Name string `xml:"name,attr" json:"name,omitempty"` 14 | Epoch string `xml:"epoch,attr" json:"epoch,omitempty"` 15 | Version string `xml:"version,attr" json:"version,omitempty"` 16 | Release string `xml:"release,attr" json:"release,omitempty"` 17 | Arch string `xml:"arch,attr" json:"arch,omitempty"` 18 | Filename string `xml:"filename" json:"filename,omitempty"` 19 | } 20 | 21 | // Updated has updated at 22 | type Updated struct { 23 | Date string `xml:"date,attr" json:"date,omitempty"` 24 | } 25 | 26 | // Issued has issued at 27 | type Issued struct { 28 | Date string `xml:"date,attr" json:"date,omitempty"` 29 | } 30 | 31 | // UpdateInfo has detailed data of Updates 32 | type UpdateInfo struct { 33 | ID string `xml:"id" json:"id,omitempty"` 34 | Title string `xml:"title" json:"title,omitempty"` 35 | Type string `xml:"type,attr" json:"type,omitempty"` 36 | Issued Issued `xml:"issued" json:"issued,omitempty"` 37 | Updated Updated `xml:"updated" json:"updated,omitempty"` 38 | Severity string `xml:"severity" json:"severity,omitempty"` 39 | Description string `xml:"description" json:"description,omitempty"` 40 | Packages []Package `xml:"pkglist>collection>package" json:"packages,omitempty"` 41 | ModularityLabel string `json:"modularity_label,omitempty"` 42 | References []Reference `xml:"references>reference" json:"references,omitempty"` 43 | CVEIDs []string `json:"cveiDs,omitempty"` 44 | } 45 | 46 | // Updates has a list of Update Info 47 | type Updates struct { 48 | UpdateList []UpdateInfo `xml:"update"` 49 | } 50 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | // Version ... Version 4 | var Version = "" 5 | 6 | // Revision of Git 7 | var Revision string 8 | 9 | const ( 10 | // RedHat is 11 | RedHat = "redhat" 12 | 13 | // CentOS is 14 | CentOS = "centos" 15 | 16 | // Debian is 17 | Debian = "debian" 18 | 19 | // Ubuntu is 20 | Ubuntu = "ubuntu" 21 | 22 | // Raspbian is 23 | Raspbian = "raspbian" 24 | 25 | // Ubuntu1404 is Ubuntu Trusty 26 | Ubuntu1404 = "trusty" 27 | 28 | // Ubuntu1604 is Ubuntu Xenial 29 | Ubuntu1604 = "xenial" 30 | 31 | // Ubuntu1804 is Ubuntu Bionic 32 | Ubuntu1804 = "bionic" 33 | 34 | // Ubuntu2004 is Focal Fossa 35 | Ubuntu2004 = "focal" 36 | 37 | // Ubuntu2104 is Hirsute Hippo 38 | Ubuntu2104 = "hirsute" 39 | 40 | // Ubuntu2110 is Impish Indri 41 | Ubuntu2110 = "impish" 42 | 43 | // Ubuntu2204 is Jammy Jellyfish 44 | Ubuntu2204 = "jammy" 45 | 46 | // Ubuntu2210 is Kinetic Kudu 47 | Ubuntu2210 = "kinetic" 48 | 49 | // Ubuntu2304 is Lunar Lobster 50 | Ubuntu2304 = "lunar" 51 | 52 | // Ubuntu2310 is Mantic Minotaur 53 | Ubuntu2310 = "mantic" 54 | 55 | // Ubuntu2404 is Noble Numbat 56 | Ubuntu2404 = "noble" 57 | 58 | // Ubuntu2410 is ⁠Oracular Oriole 59 | Ubuntu2410 = "oracular" 60 | 61 | // Ubuntu2504 is Plucky Puffin 62 | Ubuntu2504 = "plucky" 63 | 64 | // Debian7 is wheezy 65 | Debian7 = "wheezy" 66 | 67 | // Debian8 is jessie 68 | Debian8 = "jessie" 69 | 70 | // Debian9 is stretch 71 | Debian9 = "stretch" 72 | 73 | // Debian10 is buster 74 | Debian10 = "buster" 75 | 76 | // Debian11 is bullseye 77 | Debian11 = "bullseye" 78 | 79 | // Debian12 is bookworm 80 | Debian12 = "bookworm" 81 | 82 | // Debian13 is trixie 83 | Debian13 = "trixie" 84 | 85 | // OpenSUSE is 86 | OpenSUSE = "opensuse" 87 | 88 | // OpenSUSELeap is 89 | OpenSUSELeap = "opensuse.leap" 90 | 91 | // SUSEEnterpriseServer is 92 | SUSEEnterpriseServer = "suse.linux.enterprise.server" 93 | 94 | // SUSEEnterpriseDesktop is 95 | SUSEEnterpriseDesktop = "suse.linux.enterprise.desktop" 96 | 97 | // Oracle is 98 | Oracle = "oracle" 99 | 100 | // Alpine is 101 | Alpine = "alpine" 102 | 103 | // Amazon is 104 | Amazon = "amazon" 105 | 106 | // Fedora is 107 | Fedora = "fedora" 108 | ) 109 | -------------------------------------------------------------------------------- /commands/server.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/inconshreveable/log15" 7 | "github.com/spf13/cobra" 8 | "github.com/spf13/viper" 9 | "golang.org/x/xerrors" 10 | 11 | "github.com/vulsio/goval-dictionary/db" 12 | "github.com/vulsio/goval-dictionary/log" 13 | "github.com/vulsio/goval-dictionary/models" 14 | "github.com/vulsio/goval-dictionary/server" 15 | ) 16 | 17 | // ServerCmd is Subcommand for OVAL dictionary HTTP Server 18 | var serverCmd = &cobra.Command{ 19 | Use: "server", 20 | Short: "Start OVAL dictionary HTTP server", 21 | Long: `Start OVAL dictionary HTTP server`, 22 | RunE: executeServer, 23 | } 24 | 25 | func init() { 26 | RootCmd.AddCommand(serverCmd) 27 | 28 | serverCmd.PersistentFlags().String("bind", "127.0.0.1", "HTTP server bind to IP address") 29 | _ = viper.BindPFlag("bind", serverCmd.PersistentFlags().Lookup("bind")) 30 | 31 | serverCmd.PersistentFlags().String("port", "1324", "HTTP server port number") 32 | _ = viper.BindPFlag("port", serverCmd.PersistentFlags().Lookup("port")) 33 | } 34 | 35 | func executeServer(_ *cobra.Command, _ []string) (err error) { 36 | if err := log.SetLogger(viper.GetBool("log-to-file"), viper.GetString("log-dir"), viper.GetBool("debug"), viper.GetBool("log-json")); err != nil { 37 | return xerrors.Errorf("Failed to SetLogger. err: %w", err) 38 | } 39 | 40 | driver, err := db.NewDB(viper.GetString("dbtype"), viper.GetString("dbpath"), viper.GetBool("debug-sql"), db.Option{}) 41 | if err != nil { 42 | if errors.Is(err, db.ErrDBLocked) { 43 | return xerrors.Errorf("Failed to open DB. Close DB connection before fetching. err: %w", err) 44 | } 45 | return xerrors.Errorf("Failed to open DB. err: %w", err) 46 | } 47 | 48 | fetchMeta, err := driver.GetFetchMeta() 49 | if err != nil { 50 | return xerrors.Errorf("Failed to get FetchMeta from DB. err: %w", err) 51 | } 52 | if fetchMeta.OutDated() { 53 | return xerrors.Errorf("Failed to start server. err: SchemaVersion is old. SchemaVersion: %+v", map[string]uint{"latest": models.LatestSchemaVersion, "DB": fetchMeta.SchemaVersion}) 54 | } 55 | 56 | log15.Info("Starting HTTP Server...") 57 | if err = server.Start(viper.GetBool("log-to-file"), viper.GetString("log-dir"), driver); err != nil { 58 | return xerrors.Errorf("Failed to start server. err: %w", err) 59 | } 60 | 61 | return nil 62 | } 63 | -------------------------------------------------------------------------------- /models/alpine/alpine.go: -------------------------------------------------------------------------------- 1 | package alpine 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "time" 7 | 8 | "github.com/spf13/viper" 9 | 10 | "github.com/vulsio/goval-dictionary/models" 11 | ) 12 | 13 | // ConvertToModel Convert OVAL to models 14 | func ConvertToModel(data *SecDB) (defs []models.Definition) { 15 | cveIDPacks := map[string][]models.Package{} 16 | for _, pack := range data.Packages { 17 | for ver, vulnIDs := range pack.Pkg.Secfixes { 18 | for _, s := range vulnIDs { 19 | cveID := strings.Split(s, " ")[0] 20 | if !strings.HasPrefix(cveID, "CVE") { 21 | continue 22 | } 23 | 24 | if packs, ok := cveIDPacks[cveID]; ok { 25 | packs = append(packs, models.Package{ 26 | Name: pack.Pkg.Name, 27 | Version: ver, 28 | }) 29 | cveIDPacks[cveID] = packs 30 | } else { 31 | cveIDPacks[cveID] = []models.Package{{ 32 | Name: pack.Pkg.Name, 33 | Version: ver, 34 | }} 35 | } 36 | } 37 | } 38 | } 39 | 40 | for cveID, packs := range cveIDPacks { 41 | def := models.Definition{ 42 | DefinitionID: fmt.Sprintf("def-%s-%s-%s", data.Reponame, data.Distroversion, cveID), 43 | Title: cveID, 44 | Description: "", 45 | Advisory: models.Advisory{ 46 | Severity: "", 47 | Cves: []models.Cve{{CveID: cveID, Href: fmt.Sprintf("https://cve.mitre.org/cgi-bin/cvename.cgi?name=%s", cveID)}}, 48 | Bugzillas: []models.Bugzilla{}, 49 | AffectedCPEList: []models.Cpe{}, 50 | Issued: time.Date(1000, time.January, 1, 0, 0, 0, 0, time.UTC), 51 | Updated: time.Date(1000, time.January, 1, 0, 0, 0, 0, time.UTC), 52 | }, 53 | Debian: nil, 54 | AffectedPacks: packs, 55 | References: []models.Reference{ 56 | { 57 | Source: "CVE", 58 | RefID: cveID, 59 | RefURL: fmt.Sprintf("https://cve.mitre.org/cgi-bin/cvename.cgi?name=%s", cveID), 60 | }, 61 | }, 62 | } 63 | 64 | if viper.GetBool("no-details") { 65 | def.Title = "" 66 | def.Description = "" 67 | def.Advisory.Severity = "" 68 | def.Advisory.Bugzillas = []models.Bugzilla{} 69 | def.Advisory.AffectedCPEList = []models.Cpe{} 70 | def.Advisory.Issued = time.Time{} 71 | def.Advisory.Updated = time.Time{} 72 | def.References = []models.Reference{} 73 | } 74 | 75 | defs = append(defs, def) 76 | } 77 | return 78 | } 79 | -------------------------------------------------------------------------------- /models/amazon/amazon.go: -------------------------------------------------------------------------------- 1 | package amazon 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "time" 7 | 8 | "github.com/spf13/viper" 9 | 10 | "github.com/vulsio/goval-dictionary/models" 11 | "github.com/vulsio/goval-dictionary/models/util" 12 | ) 13 | 14 | // ConvertToModel Convert OVAL to models 15 | func ConvertToModel(data *Updates) (defs []models.Definition) { 16 | for _, alas := range data.UpdateList { 17 | if strings.Contains(alas.Description, "** REJECT **") { 18 | continue 19 | } 20 | 21 | cves := []models.Cve{} 22 | for _, cveID := range alas.CVEIDs { 23 | cves = append(cves, models.Cve{ 24 | CveID: cveID, 25 | Href: fmt.Sprintf("https://cve.mitre.org/cgi-bin/cvename.cgi?name=%s", cveID), 26 | }) 27 | } 28 | 29 | packs := []models.Package{} 30 | for _, pack := range alas.Packages { 31 | packs = append(packs, models.Package{ 32 | Name: pack.Name, 33 | Version: fmt.Sprintf("%s:%s-%s", pack.Epoch, pack.Version, pack.Release), 34 | Arch: pack.Arch, 35 | }) 36 | } 37 | 38 | refs := []models.Reference{} 39 | for _, ref := range alas.References { 40 | refs = append(refs, models.Reference{ 41 | Source: ref.Type, 42 | RefID: ref.ID, 43 | RefURL: ref.Href, 44 | }) 45 | } 46 | 47 | issuedAt := util.ParsedOrDefaultTime([]string{"2006-01-02 15:04", "2006-01-02 15:04:05"}, alas.Issued.Date) 48 | updatedAt := util.ParsedOrDefaultTime([]string{"2006-01-02 15:04", "2006-01-02 15:04:05"}, alas.Updated.Date) 49 | 50 | def := models.Definition{ 51 | DefinitionID: "def-" + alas.ID, 52 | Title: alas.ID, 53 | Description: alas.Description, 54 | Advisory: models.Advisory{ 55 | Severity: alas.Severity, 56 | Cves: cves, 57 | Bugzillas: []models.Bugzilla{}, 58 | AffectedCPEList: []models.Cpe{}, 59 | AffectedRepository: alas.Repository, 60 | Issued: issuedAt, 61 | Updated: updatedAt, 62 | }, 63 | Debian: nil, 64 | AffectedPacks: packs, 65 | References: refs, 66 | } 67 | 68 | if viper.GetBool("no-details") { 69 | def.Title = "" 70 | def.Description = "" 71 | def.Advisory.Severity = "" 72 | def.Advisory.Bugzillas = []models.Bugzilla{} 73 | def.Advisory.AffectedCPEList = []models.Cpe{} 74 | def.Advisory.Issued = time.Time{} 75 | def.Advisory.Updated = time.Time{} 76 | def.References = []models.Reference{} 77 | } 78 | 79 | defs = append(defs, def) 80 | } 81 | return 82 | } 83 | -------------------------------------------------------------------------------- /models/fedora/fedora.go: -------------------------------------------------------------------------------- 1 | package fedora 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "time" 7 | 8 | "github.com/spf13/viper" 9 | 10 | "github.com/vulsio/goval-dictionary/models" 11 | "github.com/vulsio/goval-dictionary/models/util" 12 | ) 13 | 14 | // ConvertToModel Convert OVAL to models 15 | func ConvertToModel(data *Updates) (defs []models.Definition) { 16 | for _, update := range data.UpdateList { 17 | if strings.Contains(update.Description, "** REJECT **") { 18 | continue 19 | } 20 | 21 | cves := []models.Cve{} 22 | for _, cveID := range update.CVEIDs { 23 | cves = append(cves, models.Cve{ 24 | CveID: cveID, 25 | Href: fmt.Sprintf("https://cve.mitre.org/cgi-bin/cvename.cgi?name=%s", cveID), 26 | }) 27 | } 28 | 29 | packs := []models.Package{} 30 | for _, pack := range update.Packages { 31 | packs = append(packs, models.Package{ 32 | Name: pack.Name, 33 | Version: fmt.Sprintf("%s:%s-%s", pack.Epoch, pack.Version, pack.Release), 34 | Arch: pack.Arch, 35 | ModularityLabel: update.ModularityLabel, 36 | }) 37 | } 38 | 39 | refs := []models.Reference{} 40 | bs := []models.Bugzilla{} 41 | for _, ref := range update.References { 42 | refs = append(refs, models.Reference{ 43 | Source: ref.Type, 44 | RefID: ref.ID, 45 | RefURL: ref.Href, 46 | }) 47 | if ref.Type == "bugzilla" { 48 | bs = append(bs, models.Bugzilla{ 49 | BugzillaID: ref.ID, 50 | URL: ref.Href, 51 | Title: ref.Title, 52 | }) 53 | } 54 | } 55 | 56 | issuedAt := util.ParsedOrDefaultTime([]string{"2006-01-02 15:04:05"}, update.Issued.Date) 57 | updatedAt := util.ParsedOrDefaultTime([]string{"2006-01-02 15:04:05"}, update.Updated.Date) 58 | def := models.Definition{ 59 | DefinitionID: "def-" + update.ID, 60 | Title: update.ID, 61 | Description: update.Description, 62 | Advisory: models.Advisory{ 63 | Severity: update.Severity, 64 | Cves: cves, 65 | Bugzillas: bs, 66 | AffectedCPEList: []models.Cpe{}, 67 | Issued: issuedAt, 68 | Updated: updatedAt, 69 | }, 70 | Debian: nil, 71 | AffectedPacks: packs, 72 | References: refs, 73 | } 74 | 75 | if viper.GetBool("no-details") { 76 | def.Title = "" 77 | def.Description = "" 78 | def.Advisory.Severity = "" 79 | def.Advisory.Bugzillas = []models.Bugzilla{} 80 | def.Advisory.AffectedCPEList = []models.Cpe{} 81 | def.Advisory.Issued = time.Time{} 82 | def.Advisory.Updated = time.Time{} 83 | def.References = []models.Reference{} 84 | } 85 | 86 | defs = append(defs, def) 87 | } 88 | return 89 | } 90 | -------------------------------------------------------------------------------- /commands/fetch-fedora.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/inconshreveable/log15" 9 | "github.com/spf13/cobra" 10 | "github.com/spf13/viper" 11 | "golang.org/x/xerrors" 12 | 13 | c "github.com/vulsio/goval-dictionary/config" 14 | "github.com/vulsio/goval-dictionary/db" 15 | fetcher "github.com/vulsio/goval-dictionary/fetcher/fedora" 16 | "github.com/vulsio/goval-dictionary/log" 17 | "github.com/vulsio/goval-dictionary/models" 18 | "github.com/vulsio/goval-dictionary/models/fedora" 19 | "github.com/vulsio/goval-dictionary/util" 20 | ) 21 | 22 | // fetchFedoraCmd is Subcommand for fetch Fedora OVAL 23 | var fetchFedoraCmd = &cobra.Command{ 24 | Use: "fedora [version]", 25 | Short: "Fetch Vulnerability dictionary from Fedora", 26 | Long: `Fetch Vulnerability dictionary from Fedora`, 27 | Args: cobra.MinimumNArgs(1), 28 | RunE: fetchFedora, 29 | Example: "$ goval-dictionary fetch fedora 37", 30 | } 31 | 32 | func init() { 33 | fetchCmd.AddCommand(fetchFedoraCmd) 34 | } 35 | 36 | func fetchFedora(_ *cobra.Command, args []string) (err error) { 37 | if err := log.SetLogger(viper.GetBool("log-to-file"), viper.GetString("log-dir"), viper.GetBool("debug"), viper.GetBool("log-json")); err != nil { 38 | return xerrors.Errorf("Failed to SetLogger. err: %w", err) 39 | } 40 | 41 | driver, err := db.NewDB(viper.GetString("dbtype"), viper.GetString("dbpath"), viper.GetBool("debug-sql"), db.Option{}) 42 | if err != nil { 43 | if errors.Is(err, db.ErrDBLocked) { 44 | return xerrors.Errorf("Failed to open DB. Close DB connection before fetching. err: %w", err) 45 | } 46 | return xerrors.Errorf("Failed to open DB. err: %w", err) 47 | } 48 | 49 | fetchMeta, err := driver.GetFetchMeta() 50 | if err != nil { 51 | return xerrors.Errorf("Failed to get FetchMeta from DB. err: %w", err) 52 | } 53 | if fetchMeta.OutDated() { 54 | return xerrors.Errorf("Failed to Insert CVEs into DB. SchemaVersion is old. SchemaVersion: %+v", map[string]uint{"latest": models.LatestSchemaVersion, "DB": fetchMeta.SchemaVersion}) 55 | } 56 | // If the fetch fails the first time (without SchemaVersion), the DB needs to be cleaned every time, so insert SchemaVersion. 57 | if err := driver.UpsertFetchMeta(fetchMeta); err != nil { 58 | return xerrors.Errorf("Failed to upsert FetchMeta to DB. err: %w", err) 59 | } 60 | 61 | uinfos, err := fetcher.FetchUpdateInfosFedora(util.Unique(args)) 62 | if err != nil { 63 | return xerrors.Errorf("Failed to fetch files. err: %w", err) 64 | } 65 | 66 | for k, v := range uinfos { 67 | root := models.Root{ 68 | Family: c.Fedora, 69 | OSVersion: k, 70 | Definitions: fedora.ConvertToModel(v), 71 | Timestamp: time.Now(), 72 | } 73 | log15.Info(fmt.Sprintf("%d CVEs for Fedora %s. Inserting to DB", len(root.Definitions), k)) 74 | if err := driver.InsertOval(&root); err != nil { 75 | return xerrors.Errorf("Failed to insert OVAL. err: %w", err) 76 | } 77 | log15.Info("Finish", "Updated", len(root.Definitions)) 78 | } 79 | return nil 80 | } 81 | -------------------------------------------------------------------------------- /commands/root.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/inconshreveable/log15" 9 | homedir "github.com/mitchellh/go-homedir" 10 | "github.com/spf13/cobra" 11 | "github.com/spf13/viper" 12 | 13 | "github.com/vulsio/goval-dictionary/log" 14 | ) 15 | 16 | var cfgFile string 17 | 18 | // RootCmd represents the base command when called without any subcommands 19 | var RootCmd = &cobra.Command{ 20 | Use: "goval-dictionary", 21 | Short: "OVAL(Open Vulnerability and Assessment Language) dictionary", 22 | Long: `OVAL(Open Vulnerability and Assessment Language) dictionary`, 23 | SilenceErrors: true, 24 | SilenceUsage: true, 25 | } 26 | 27 | func init() { 28 | cobra.OnInitialize(initConfig) 29 | 30 | RootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.oval.yaml)") 31 | 32 | RootCmd.PersistentFlags().Bool("log-to-file", false, "output log to file") 33 | _ = viper.BindPFlag("log-to-file", RootCmd.PersistentFlags().Lookup("log-to-file")) 34 | 35 | RootCmd.PersistentFlags().String("log-dir", log.GetDefaultLogDir(), "/path/to/log") 36 | _ = viper.BindPFlag("log-dir", RootCmd.PersistentFlags().Lookup("log-dir")) 37 | 38 | RootCmd.PersistentFlags().Bool("log-json", false, "output log as JSON") 39 | _ = viper.BindPFlag("log-json", RootCmd.PersistentFlags().Lookup("log-json")) 40 | 41 | RootCmd.PersistentFlags().Bool("debug", false, "debug mode (default: false)") 42 | _ = viper.BindPFlag("debug", RootCmd.PersistentFlags().Lookup("debug")) 43 | 44 | RootCmd.PersistentFlags().Bool("debug-sql", false, "SQL debug mode") 45 | _ = viper.BindPFlag("debug-sql", RootCmd.PersistentFlags().Lookup("debug-sql")) 46 | 47 | pwd := os.Getenv("PWD") 48 | RootCmd.PersistentFlags().String("dbpath", filepath.Join(pwd, "oval.sqlite3"), "/path/to/sqlite3 or SQL connection string") 49 | _ = viper.BindPFlag("dbpath", RootCmd.PersistentFlags().Lookup("dbpath")) 50 | 51 | RootCmd.PersistentFlags().String("dbtype", "sqlite3", "Database type to store data in (sqlite3, mysql, postgres or redis supported)") 52 | _ = viper.BindPFlag("dbtype", RootCmd.PersistentFlags().Lookup("dbtype")) 53 | 54 | RootCmd.PersistentFlags().String("http-proxy", "", "http://proxy-url:port (default: empty)") 55 | _ = viper.BindPFlag("http-proxy", RootCmd.PersistentFlags().Lookup("http-proxy")) 56 | } 57 | 58 | // initConfig reads in config file and ENV variables if set. 59 | func initConfig() { 60 | if cfgFile != "" { 61 | viper.SetConfigFile(cfgFile) 62 | } else { 63 | // Find home directory. 64 | home, err := homedir.Dir() 65 | if err != nil { 66 | log15.Error("Failed to find home directory.", "err", err) 67 | os.Exit(1) 68 | } 69 | 70 | // Search config in home directory with name ".goval-dictionary" (without extension). 71 | viper.AddConfigPath(home) 72 | viper.SetConfigName(".goval-dictionary") 73 | } 74 | 75 | viper.AutomaticEnv() // read in environment variables that match 76 | 77 | // If a config file is found, read it in. 78 | if err := viper.ReadInConfig(); err == nil { 79 | fmt.Println("Using config file:", viper.ConfigFileUsed()) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /commands/fetch-amazon.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | 7 | "github.com/inconshreveable/log15" 8 | "github.com/spf13/cobra" 9 | "github.com/spf13/viper" 10 | "golang.org/x/xerrors" 11 | 12 | c "github.com/vulsio/goval-dictionary/config" 13 | "github.com/vulsio/goval-dictionary/db" 14 | fetcher "github.com/vulsio/goval-dictionary/fetcher/amazon" 15 | "github.com/vulsio/goval-dictionary/log" 16 | "github.com/vulsio/goval-dictionary/models" 17 | "github.com/vulsio/goval-dictionary/models/amazon" 18 | "github.com/vulsio/goval-dictionary/util" 19 | ) 20 | 21 | // fetchAmazonCmd is Subcommand for fetch Amazon ALAS RSS 22 | // https://alas.aws.amazon.com/alas.rss 23 | var fetchAmazonCmd = &cobra.Command{ 24 | Use: "amazon [version]", 25 | Short: "Fetch Vulnerability dictionary from Amazon ALAS", 26 | Long: `Fetch Vulnerability dictionary from Amazon ALAS`, 27 | Args: cobra.MinimumNArgs(1), 28 | RunE: fetchAmazon, 29 | Example: "$ goval-dictionary fetch amazon 1 2 2022 2023", 30 | } 31 | 32 | func init() { 33 | fetchCmd.AddCommand(fetchAmazonCmd) 34 | } 35 | 36 | func fetchAmazon(_ *cobra.Command, args []string) (err error) { 37 | if err := log.SetLogger(viper.GetBool("log-to-file"), viper.GetString("log-dir"), viper.GetBool("debug"), viper.GetBool("log-json")); err != nil { 38 | return xerrors.Errorf("Failed to SetLogger. err: %w", err) 39 | } 40 | 41 | driver, err := db.NewDB(viper.GetString("dbtype"), viper.GetString("dbpath"), viper.GetBool("debug-sql"), db.Option{}) 42 | if err != nil { 43 | if errors.Is(err, db.ErrDBLocked) { 44 | return xerrors.Errorf("Failed to open DB. Close DB connection before fetching. err: %w", err) 45 | } 46 | return xerrors.Errorf("Failed to open DB. err: %w", err) 47 | } 48 | 49 | fetchMeta, err := driver.GetFetchMeta() 50 | if err != nil { 51 | return xerrors.Errorf("Failed to get FetchMeta from DB. err: %w", err) 52 | } 53 | if fetchMeta.OutDated() { 54 | return xerrors.Errorf("Failed to Insert CVEs into DB. SchemaVersion is old. SchemaVersion: %+v", map[string]uint{"latest": models.LatestSchemaVersion, "DB": fetchMeta.SchemaVersion}) 55 | } 56 | // If the fetch fails the first time (without SchemaVersion), the DB needs to be cleaned every time, so insert SchemaVersion. 57 | if err := driver.UpsertFetchMeta(fetchMeta); err != nil { 58 | return xerrors.Errorf("Failed to upsert FetchMeta to DB. err: %w", err) 59 | } 60 | 61 | m, err := fetcher.FetchFiles(util.Unique(args)) 62 | if err != nil { 63 | return xerrors.Errorf("Failed to fetch files. err: %w", err) 64 | } 65 | for ver, us := range m { 66 | root := models.Root{ 67 | Family: c.Amazon, 68 | OSVersion: ver, 69 | Definitions: amazon.ConvertToModel(us), 70 | Timestamp: time.Now(), 71 | } 72 | 73 | if err := driver.InsertOval(&root); err != nil { 74 | return xerrors.Errorf("Failed to insert OVAL. err: %w", err) 75 | } 76 | log15.Info("Finish", "Updated", len(root.Definitions)) 77 | } 78 | 79 | fetchMeta.LastFetchedAt = time.Now() 80 | if err := driver.UpsertFetchMeta(fetchMeta); err != nil { 81 | return xerrors.Errorf("Failed to upsert FetchMeta to DB. err: %w", err) 82 | } 83 | 84 | return nil 85 | } 86 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/vulsio/goval-dictionary 2 | 3 | go 1.24.0 4 | 5 | require ( 6 | github.com/cheggaaa/pb/v3 v3.1.7 7 | github.com/glebarez/sqlite v1.11.0 8 | github.com/go-redis/redis/v8 v8.11.5 9 | github.com/google/go-cmp v0.7.0 10 | github.com/hashicorp/go-version v1.8.0 11 | github.com/inconshreveable/log15 v3.0.0-testing.5+incompatible 12 | github.com/k0kubun/pp v3.0.1+incompatible 13 | github.com/klauspost/compress v1.18.2 14 | github.com/knqyf263/go-rpm-version v0.0.0-20220614171824-631e686d1075 15 | github.com/labstack/echo/v4 v4.13.4 16 | github.com/mitchellh/go-homedir v1.1.0 17 | github.com/spf13/cobra v1.10.2 18 | github.com/spf13/viper v1.21.0 19 | github.com/ulikunitz/xz v0.5.15 20 | golang.org/x/net v0.47.0 21 | golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 22 | gopkg.in/yaml.v2 v2.4.0 23 | gorm.io/driver/mysql v1.5.5 24 | gorm.io/driver/postgres v1.5.7 25 | gorm.io/gorm v1.25.7 26 | ) 27 | 28 | require ( 29 | github.com/VividCortex/ewma v1.2.0 // indirect 30 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 31 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 32 | github.com/dustin/go-humanize v1.0.1 // indirect 33 | github.com/fatih/color v1.18.0 // indirect 34 | github.com/fsnotify/fsnotify v1.9.0 // indirect 35 | github.com/glebarez/go-sqlite v1.21.2 // indirect 36 | github.com/go-sql-driver/mysql v1.7.1 // indirect 37 | github.com/go-stack/stack v1.8.0 // indirect 38 | github.com/go-viper/mapstructure/v2 v2.4.0 // indirect 39 | github.com/google/uuid v1.6.0 // indirect 40 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 41 | github.com/jackc/pgpassfile v1.0.0 // indirect 42 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect 43 | github.com/jackc/pgx/v5 v5.5.4 // indirect 44 | github.com/jackc/puddle/v2 v2.2.1 // indirect 45 | github.com/jinzhu/inflection v1.0.0 // indirect 46 | github.com/jinzhu/now v1.1.5 // indirect 47 | github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 // indirect 48 | github.com/labstack/gommon v0.4.2 // indirect 49 | github.com/mattn/go-colorable v0.1.14 // indirect 50 | github.com/mattn/go-isatty v0.0.20 // indirect 51 | github.com/mattn/go-runewidth v0.0.16 // indirect 52 | github.com/pelletier/go-toml/v2 v2.2.4 // indirect 53 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect 54 | github.com/rivo/uniseg v0.4.7 // indirect 55 | github.com/sagikazarmark/locafero v0.11.0 // indirect 56 | github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect 57 | github.com/spf13/afero v1.15.0 // indirect 58 | github.com/spf13/cast v1.10.0 // indirect 59 | github.com/spf13/pflag v1.0.10 // indirect 60 | github.com/subosito/gotenv v1.6.0 // indirect 61 | github.com/valyala/bytebufferpool v1.0.0 // indirect 62 | github.com/valyala/fasttemplate v1.2.2 // indirect 63 | go.yaml.in/yaml/v3 v3.0.4 // indirect 64 | golang.org/x/crypto v0.45.0 // indirect 65 | golang.org/x/sync v0.18.0 // indirect 66 | golang.org/x/sys v0.38.0 // indirect 67 | golang.org/x/term v0.37.0 // indirect 68 | golang.org/x/text v0.31.0 // indirect 69 | golang.org/x/time v0.11.0 // indirect 70 | modernc.org/libc v1.22.5 // indirect 71 | modernc.org/mathutil v1.5.0 // indirect 72 | modernc.org/memory v1.5.0 // indirect 73 | modernc.org/sqlite v1.23.1 // indirect 74 | ) 75 | -------------------------------------------------------------------------------- /fetcher/ubuntu/ubuntu.go: -------------------------------------------------------------------------------- 1 | package ubuntu 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/inconshreveable/log15" 8 | "golang.org/x/xerrors" 9 | 10 | "github.com/vulsio/goval-dictionary/config" 11 | "github.com/vulsio/goval-dictionary/fetcher/util" 12 | ) 13 | 14 | func newFetchRequests(target []string) (reqs []util.FetchRequest) { 15 | for _, v := range target { 16 | switch url := getOVALURL(v); url { 17 | case "unknown": 18 | log15.Warn("Skip unknown ubuntu.", "version", v) 19 | case "unsupported": 20 | log15.Warn("Skip unsupported ubuntu version.", "version", v) 21 | log15.Warn("See https://wiki.ubuntu.com/Releases for supported versions") 22 | default: 23 | reqs = append(reqs, util.FetchRequest{ 24 | Target: v, 25 | URL: url, 26 | MIMEType: util.MIMETypeBzip2, 27 | }) 28 | } 29 | } 30 | return 31 | } 32 | 33 | func getOVALURL(version string) string { 34 | major, minor, ok := strings.Cut(version, ".") 35 | if !ok { 36 | return "unknown" 37 | } 38 | 39 | const main = "https://security-metadata.canonical.com/oval/oci.com.ubuntu.%s.cve.oval.xml.bz2" 40 | switch major { 41 | case "4", "5", "6", "7", "8", "9", "10", "11", "12": 42 | return "unsupported" 43 | case "14": 44 | switch minor { 45 | case "04": 46 | return fmt.Sprintf(main, config.Ubuntu1404) 47 | case "10": 48 | return "unsupported" 49 | default: 50 | return "unknown" 51 | } 52 | case "16": 53 | switch minor { 54 | case "04": 55 | return fmt.Sprintf(main, config.Ubuntu1604) 56 | case "10": 57 | return "unsupported" 58 | default: 59 | return "unknown" 60 | } 61 | case "17": 62 | return "unsupported" 63 | case "18": 64 | switch minor { 65 | case "04": 66 | return fmt.Sprintf(main, config.Ubuntu1804) 67 | case "10": 68 | return "unsupported" 69 | default: 70 | return "unknown" 71 | } 72 | case "19": 73 | return "unsupported" 74 | case "20": 75 | switch minor { 76 | case "04": 77 | return fmt.Sprintf(main, config.Ubuntu2004) 78 | case "10": 79 | return "unsupported" 80 | default: 81 | return "unknown" 82 | } 83 | case "21": 84 | return "unsupported" 85 | case "22": 86 | switch minor { 87 | case "04": 88 | return fmt.Sprintf(main, config.Ubuntu2204) 89 | case "10": 90 | return "unsupported" 91 | default: 92 | return "unknown" 93 | } 94 | case "23": 95 | return "unsupported" 96 | case "24": 97 | switch minor { 98 | case "04": 99 | return fmt.Sprintf(main, config.Ubuntu2404) 100 | case "10": 101 | return fmt.Sprintf(main, config.Ubuntu2410) 102 | default: 103 | return "unknown" 104 | } 105 | case "25": 106 | switch minor { 107 | case "04": 108 | return fmt.Sprintf(main, config.Ubuntu2504) 109 | default: 110 | return "unknown" 111 | } 112 | default: 113 | return "unknown" 114 | } 115 | } 116 | 117 | // FetchFiles fetch OVAL from Ubuntu 118 | func FetchFiles(versions []string) ([]util.FetchResult, error) { 119 | reqs := newFetchRequests(versions) 120 | if len(reqs) == 0 { 121 | return nil, xerrors.New("There are no versions to fetch") 122 | } 123 | 124 | results := make([]util.FetchResult, 0, len(reqs)) 125 | for _, req := range reqs { 126 | rs, err := util.FetchFeedFiles([]util.FetchRequest{req}) 127 | if err != nil { 128 | return nil, xerrors.Errorf("Failed to fetch. err: %w", err) 129 | } 130 | results = append(results, rs...) 131 | } 132 | return results, nil 133 | } 134 | -------------------------------------------------------------------------------- /commands/fetch-alpine.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | 7 | "golang.org/x/xerrors" 8 | yaml "gopkg.in/yaml.v2" 9 | 10 | "github.com/inconshreveable/log15" 11 | "github.com/spf13/cobra" 12 | "github.com/spf13/viper" 13 | 14 | c "github.com/vulsio/goval-dictionary/config" 15 | "github.com/vulsio/goval-dictionary/db" 16 | fetcher "github.com/vulsio/goval-dictionary/fetcher/alpine" 17 | "github.com/vulsio/goval-dictionary/log" 18 | "github.com/vulsio/goval-dictionary/models" 19 | "github.com/vulsio/goval-dictionary/models/alpine" 20 | "github.com/vulsio/goval-dictionary/util" 21 | ) 22 | 23 | // fetchAlpineCmd is Subcommand for fetch Alpine secdb 24 | // https://secdb.alpinelinux.org/ 25 | var fetchAlpineCmd = &cobra.Command{ 26 | Use: "alpine [version]", 27 | Short: "Fetch Vulnerability dictionary from Alpine secdb", 28 | Long: `Fetch Vulnerability dictionary from Alpine secdb`, 29 | Args: cobra.MinimumNArgs(1), 30 | RunE: fetchAlpine, 31 | Example: "$ goval-dictionary fetch alpine 3.16 3.17", 32 | } 33 | 34 | func init() { 35 | fetchCmd.AddCommand(fetchAlpineCmd) 36 | } 37 | 38 | func fetchAlpine(_ *cobra.Command, args []string) (err error) { 39 | if err := log.SetLogger(viper.GetBool("log-to-file"), viper.GetString("log-dir"), viper.GetBool("debug"), viper.GetBool("log-json")); err != nil { 40 | return xerrors.Errorf("Failed to SetLogger. err: %w", err) 41 | } 42 | 43 | driver, err := db.NewDB(viper.GetString("dbtype"), viper.GetString("dbpath"), viper.GetBool("debug-sql"), db.Option{}) 44 | if err != nil { 45 | if errors.Is(err, db.ErrDBLocked) { 46 | return xerrors.Errorf("Failed to open DB. Close DB connection before fetching. err: %w", err) 47 | } 48 | return xerrors.Errorf("Failed to open DB. err: %w", err) 49 | } 50 | 51 | fetchMeta, err := driver.GetFetchMeta() 52 | if err != nil { 53 | return xerrors.Errorf("Failed to get FetchMeta from DB. err: %w", err) 54 | } 55 | if fetchMeta.OutDated() { 56 | return xerrors.Errorf("Failed to Insert CVEs into DB. err: SchemaVersion is old. SchemaVersion: %+v", map[string]uint{"latest": models.LatestSchemaVersion, "DB": fetchMeta.SchemaVersion}) 57 | } 58 | // If the fetch fails the first time (without SchemaVersion), the DB needs to be cleaned every time, so insert SchemaVersion. 59 | if err := driver.UpsertFetchMeta(fetchMeta); err != nil { 60 | return xerrors.Errorf("Failed to upsert FetchMeta to DB. err: %w", err) 61 | } 62 | 63 | results, err := fetcher.FetchFiles(util.Unique(args)) 64 | if err != nil { 65 | return xerrors.Errorf("Failed to fetch files. err: %w", err) 66 | } 67 | 68 | osVerDefs := map[string][]models.Definition{} 69 | for _, r := range results { 70 | var secdb alpine.SecDB 71 | if err := yaml.Unmarshal(r.Body, &secdb); err != nil { 72 | return xerrors.Errorf("Failed to unmarshal. err: %w", err) 73 | } 74 | osVerDefs[r.Target] = append(osVerDefs[r.Target], alpine.ConvertToModel(&secdb)...) 75 | } 76 | 77 | for osVer, defs := range osVerDefs { 78 | root := models.Root{ 79 | Family: c.Alpine, 80 | OSVersion: osVer, 81 | Definitions: defs, 82 | Timestamp: time.Now(), 83 | } 84 | if err := driver.InsertOval(&root); err != nil { 85 | return xerrors.Errorf("Failed to insert OVAL. err: %w", err) 86 | } 87 | log15.Info("Finish", "Updated", len(root.Definitions)) 88 | } 89 | 90 | fetchMeta.LastFetchedAt = time.Now() 91 | if err := driver.UpsertFetchMeta(fetchMeta); err != nil { 92 | return xerrors.Errorf("Failed to upsert FetchMeta to DB. err: %w", err) 93 | } 94 | 95 | return nil 96 | } 97 | -------------------------------------------------------------------------------- /models/debian/debian.go: -------------------------------------------------------------------------------- 1 | package debian 2 | 3 | import ( 4 | "strings" 5 | "time" 6 | 7 | "github.com/spf13/viper" 8 | 9 | "github.com/vulsio/goval-dictionary/models" 10 | "github.com/vulsio/goval-dictionary/models/util" 11 | ) 12 | 13 | type distroPackage struct { 14 | osVer string 15 | pack models.Package 16 | } 17 | 18 | // ConvertToModel Convert OVAL to models 19 | func ConvertToModel(root *Root) (defs []models.Definition) { 20 | for _, ovaldef := range root.Definitions.Definitions { 21 | if strings.Contains(ovaldef.Description, "** REJECT **") { 22 | continue 23 | } 24 | 25 | cves := []models.Cve{} 26 | rs := []models.Reference{} 27 | for _, r := range ovaldef.References { 28 | if r.Source == "CVE" { 29 | cves = append(cves, models.Cve{ 30 | CveID: r.RefID, 31 | Href: r.RefURL, 32 | }) 33 | } 34 | 35 | rs = append(rs, models.Reference{ 36 | Source: r.Source, 37 | RefID: r.RefID, 38 | RefURL: r.RefURL, 39 | }) 40 | } 41 | 42 | def := models.Definition{ 43 | DefinitionID: ovaldef.ID, 44 | Title: ovaldef.Title, 45 | Description: ovaldef.Description, 46 | Advisory: models.Advisory{ 47 | Severity: "", 48 | Cves: cves, 49 | Bugzillas: []models.Bugzilla{}, 50 | AffectedCPEList: []models.Cpe{}, 51 | Issued: time.Date(1000, time.January, 1, 0, 0, 0, 0, time.UTC), 52 | Updated: time.Date(1000, time.January, 1, 0, 0, 0, 0, time.UTC), 53 | }, 54 | Debian: &models.Debian{ 55 | DSA: ovaldef.Debian.DSA, 56 | MoreInfo: ovaldef.Debian.MoreInfo, 57 | Date: util.ParsedOrDefaultTime([]string{"2006-01-02"}, ovaldef.Debian.Date), 58 | }, 59 | AffectedPacks: collectDebianPacks(ovaldef.Criteria), 60 | References: rs, 61 | } 62 | 63 | if viper.GetBool("no-details") { 64 | def.Title = "" 65 | def.Description = "" 66 | def.Advisory.Severity = "" 67 | def.Advisory.Bugzillas = []models.Bugzilla{} 68 | def.Advisory.AffectedCPEList = []models.Cpe{} 69 | def.Advisory.Issued = time.Time{} 70 | def.Advisory.Updated = time.Time{} 71 | def.Debian = nil 72 | def.References = []models.Reference{} 73 | } 74 | 75 | defs = append(defs, def) 76 | } 77 | return 78 | } 79 | 80 | func collectDebianPacks(cri Criteria) []models.Package { 81 | distPacks := walkDebian(cri, "", []distroPackage{}) 82 | packs := make([]models.Package, len(distPacks)) 83 | for i, distPack := range distPacks { 84 | packs[i] = distPack.pack 85 | } 86 | return packs 87 | } 88 | 89 | func walkDebian(cri Criteria, osVer string, acc []distroPackage) []distroPackage { 90 | for _, c := range cri.Criterions { 91 | if strings.HasPrefix(c.Comment, "Debian ") && 92 | strings.HasSuffix(c.Comment, " is installed") { 93 | osVer = strings.TrimSuffix(strings.TrimPrefix(c.Comment, "Debian "), " is installed") 94 | } 95 | ss := strings.Split(c.Comment, " DPKG is earlier than ") 96 | if len(ss) != 2 { 97 | continue 98 | } 99 | 100 | // "0" means notyetfixed or erroneous information. 101 | // Not available because "0" includes erroneous info... 102 | if ss[1] == "0" { 103 | continue 104 | } 105 | acc = append(acc, distroPackage{ 106 | osVer: osVer, 107 | pack: models.Package{ 108 | Name: ss[0], 109 | Version: strings.Split(ss[1], " ")[0], 110 | }, 111 | }) 112 | } 113 | 114 | if len(cri.Criterias) == 0 { 115 | return acc 116 | } 117 | for _, c := range cri.Criterias { 118 | acc = walkDebian(c, osVer, acc) 119 | } 120 | return acc 121 | } 122 | -------------------------------------------------------------------------------- /fetcher/fedora/types.go: -------------------------------------------------------------------------------- 1 | package fedora 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "golang.org/x/xerrors" 8 | 9 | models "github.com/vulsio/goval-dictionary/models/fedora" 10 | ) 11 | 12 | // repoMd has repomd data 13 | type repoMd struct { 14 | RepoList []repo `xml:"data"` 15 | } 16 | 17 | // repo has a repo data 18 | type repo struct { 19 | Type string `xml:"type,attr"` 20 | Location location `xml:"location"` 21 | } 22 | 23 | // location has a location of repomd 24 | type location struct { 25 | Href string `xml:"href,attr"` 26 | } 27 | 28 | type bugzillaXML struct { 29 | Blocked []string `xml:"bug>blocked" json:"blocked,omitempty"` 30 | Alias string `xml:"bug>alias" json:"alias,omitempty"` 31 | } 32 | 33 | // moduleInfo has a data of modules.yaml 34 | type moduleInfo struct { 35 | Version int `yaml:"version"` 36 | Data struct { 37 | Name string `yaml:"name"` 38 | Stream string `yaml:"stream"` 39 | Version int64 `yaml:"version"` 40 | Context string `yaml:"context"` 41 | Arch string `yaml:"arch"` 42 | Artifacts struct { 43 | Rpms []Rpm `yaml:"rpms"` 44 | } `yaml:"artifacts"` 45 | } `yaml:"data"` 46 | } 47 | 48 | type moduleInfosPerVersion map[string]moduleInfosPerPackage 49 | 50 | type moduleInfosPerPackage map[string]moduleInfo 51 | 52 | // ConvertToUpdateInfoTitle generates file name from data of modules.yaml 53 | func (f moduleInfo) ConvertToUpdateInfoTitle() string { 54 | return fmt.Sprintf("%s-%s-%d.%s", f.Data.Name, f.Data.Stream, f.Data.Version, f.Data.Context) 55 | } 56 | 57 | // ConvertToModularityLabel generates modularity_label from data of modules.yaml 58 | func (f moduleInfo) ConvertToModularityLabel() string { 59 | return fmt.Sprintf("%s:%s:%d:%s", f.Data.Name, f.Data.Stream, f.Data.Version, f.Data.Context) 60 | } 61 | 62 | // Rpm is a package name of data/artifacts/rpms in modules.yaml 63 | type Rpm string 64 | 65 | // NewPackageFromRpm generates Package{} by parsing package name 66 | func (r Rpm) NewPackageFromRpm() (models.Package, error) { 67 | filename := strings.TrimSuffix(string(r), ".rpm") 68 | 69 | archIndex := strings.LastIndex(filename, ".") 70 | if archIndex == -1 { 71 | return models.Package{}, xerrors.Errorf("Failed to parse arch from filename: %s", filename) 72 | } 73 | arch := filename[archIndex+1:] 74 | 75 | relIndex := strings.LastIndex(filename[:archIndex], "-") 76 | if relIndex == -1 { 77 | return models.Package{}, xerrors.Errorf("Failed to parse release from filename: %s", filename) 78 | } 79 | rel := filename[relIndex+1 : archIndex] 80 | 81 | verIndex := strings.LastIndex(filename[:relIndex], "-") 82 | if verIndex == -1 { 83 | return models.Package{}, xerrors.Errorf("Failed to parse version from filename: %s", filename) 84 | } 85 | ver := filename[verIndex+1 : relIndex] 86 | 87 | epochIndex := strings.Index(ver, ":") 88 | var epoch string 89 | if epochIndex == -1 { 90 | epoch = "0" 91 | } else { 92 | epoch = ver[:epochIndex] 93 | ver = ver[epochIndex+1:] 94 | } 95 | 96 | name := filename[:verIndex] 97 | pkg := models.Package{ 98 | Name: name, 99 | Epoch: epoch, 100 | Version: ver, 101 | Release: rel, 102 | Arch: arch, 103 | Filename: filename, 104 | } 105 | return pkg, nil 106 | } 107 | 108 | // uniquePackages returns deduplicated []Package by Filename 109 | // If Filename is the same, all other information is considered to be the same 110 | func uniquePackages(pkgs []models.Package) []models.Package { 111 | tmp := make(map[string]models.Package) 112 | ret := []models.Package{} 113 | for _, pkg := range pkgs { 114 | tmp[pkg.Filename] = pkg 115 | } 116 | for _, v := range tmp { 117 | ret = append(ret, v) 118 | } 119 | return ret 120 | } 121 | 122 | func mergeUpdates(source map[string]*models.Updates, target map[string]*models.Updates) map[string]*models.Updates { 123 | for osVer, sourceUpdates := range source { 124 | if targetUpdates, ok := target[osVer]; ok { 125 | source[osVer].UpdateList = append(sourceUpdates.UpdateList, targetUpdates.UpdateList...) 126 | } 127 | } 128 | return source 129 | } 130 | -------------------------------------------------------------------------------- /db/db.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "strings" 5 | "time" 6 | 7 | "golang.org/x/xerrors" 8 | 9 | c "github.com/vulsio/goval-dictionary/config" 10 | "github.com/vulsio/goval-dictionary/models" 11 | ) 12 | 13 | // DB is interface for a database driver 14 | type DB interface { 15 | Name() string 16 | OpenDB(string, string, bool, Option) error 17 | CloseDB() error 18 | MigrateDB() error 19 | 20 | IsGovalDictModelV1() (bool, error) 21 | GetFetchMeta() (*models.FetchMeta, error) 22 | UpsertFetchMeta(*models.FetchMeta) error 23 | 24 | GetByPackName(family string, osVer string, packName string, arch string) ([]models.Definition, error) 25 | GetByCveID(family string, osVer string, cveID string, arch string) ([]models.Definition, error) 26 | GetAdvisories(family string, osVer string) (map[string][]string, error) 27 | InsertOval(*models.Root) error 28 | CountDefs(string, string) (int, error) 29 | GetLastModified(string, string) (time.Time, error) 30 | } 31 | 32 | // Option : 33 | type Option struct { 34 | RedisTimeout time.Duration 35 | } 36 | 37 | // NewDB return DB accessor. 38 | func NewDB(dbType, dbPath string, debugSQL bool, option Option) (driver DB, err error) { 39 | if driver, err = newDB(dbType); err != nil { 40 | return driver, xerrors.Errorf("Failed to new db. err: %w", err) 41 | } 42 | 43 | if err := driver.OpenDB(dbType, dbPath, debugSQL, option); err != nil { 44 | return nil, xerrors.Errorf("Failed to open db. err: %w", err) 45 | } 46 | 47 | isV1, err := driver.IsGovalDictModelV1() 48 | if err != nil { 49 | return nil, xerrors.Errorf("Failed to IsGovalDictModelV1. err: %w", err) 50 | } 51 | if isV1 { 52 | return nil, xerrors.New("Failed to NewDB. Since SchemaVersion is incompatible, delete Database and fetch again.") 53 | } 54 | 55 | if err := driver.MigrateDB(); err != nil { 56 | return driver, xerrors.Errorf("Failed to migrate db. err: %w", err) 57 | } 58 | return driver, nil 59 | } 60 | 61 | func newDB(dbType string) (DB, error) { 62 | switch dbType { 63 | case dialectSqlite3, dialectMysql, dialectPostgreSQL: 64 | return &RDBDriver{name: dbType}, nil 65 | case dialectRedis: 66 | return &RedisDriver{name: dbType}, nil 67 | } 68 | return nil, xerrors.Errorf("Invalid database dialect. dbType: %s", dbType) 69 | } 70 | 71 | func formatFamilyAndOSVer(family, osVer string) (string, string, error) { 72 | switch family { 73 | case c.Debian: 74 | osVer = major(osVer) 75 | case c.Ubuntu: 76 | osVer = majorDotMinor(osVer) 77 | case c.Raspbian: 78 | family = c.Debian 79 | osVer = major(osVer) 80 | case c.RedHat: 81 | osVer = major(osVer) 82 | case c.CentOS: 83 | family = c.RedHat 84 | osVer = major(osVer) 85 | case c.Oracle: 86 | osVer = major(osVer) 87 | case c.Amazon: 88 | osVer = getAmazonLinuxVer(osVer) 89 | case c.Alpine: 90 | osVer = majorDotMinor(osVer) 91 | case c.Fedora: 92 | osVer = major(osVer) 93 | case c.OpenSUSE: 94 | if osVer != "tumbleweed" { 95 | osVer = majorDotMinor(osVer) 96 | } 97 | case c.OpenSUSELeap, c.SUSEEnterpriseDesktop, c.SUSEEnterpriseServer: 98 | osVer = majorDotMinor(osVer) 99 | default: 100 | return "", "", xerrors.Errorf("Failed to detect family. err: unknown os family(%s)", family) 101 | } 102 | 103 | return family, osVer, nil 104 | } 105 | 106 | func major(osVer string) (majorVersion string) { 107 | return strings.Split(osVer, ".")[0] 108 | } 109 | 110 | func majorDotMinor(osVer string) (majorMinorVersion string) { 111 | ss := strings.Split(osVer, ".") 112 | if len(ss) < 3 { 113 | return osVer 114 | } 115 | return strings.Join(ss[:2], ".") 116 | } 117 | 118 | // getAmazonLinuxVer returns AmazonLinux 1, 2, 2022, 2023 119 | func getAmazonLinuxVer(osVersion string) string { 120 | ss := strings.Fields(osVersion) 121 | if ss[0] == "2023" { 122 | return "2023" 123 | } 124 | if ss[0] == "2022" { 125 | return "2022" 126 | } 127 | if ss[0] == "2" { 128 | return "2" 129 | } 130 | return "1" 131 | } 132 | 133 | func filterByRedHatMajor(packs []models.Package, majorVer string) (filtered []models.Package) { 134 | for _, p := range packs { 135 | if p.NotFixedYet || 136 | strings.Contains(p.Version, ".el"+majorVer) || strings.Contains(p.Version, ".module+el"+majorVer) { 137 | filtered = append(filtered, p) 138 | } 139 | } 140 | return 141 | } 142 | -------------------------------------------------------------------------------- /commands/fetch-ubuntu.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "encoding/xml" 5 | "errors" 6 | "strings" 7 | "time" 8 | 9 | "github.com/inconshreveable/log15" 10 | "github.com/spf13/cobra" 11 | "github.com/spf13/viper" 12 | "golang.org/x/xerrors" 13 | 14 | c "github.com/vulsio/goval-dictionary/config" 15 | "github.com/vulsio/goval-dictionary/db" 16 | fetcher "github.com/vulsio/goval-dictionary/fetcher/ubuntu" 17 | "github.com/vulsio/goval-dictionary/log" 18 | "github.com/vulsio/goval-dictionary/models" 19 | "github.com/vulsio/goval-dictionary/models/ubuntu" 20 | "github.com/vulsio/goval-dictionary/util" 21 | ) 22 | 23 | // fetchUbuntuCmd is Subcommand for fetch Ubuntu OVAL 24 | var fetchUbuntuCmd = &cobra.Command{ 25 | Use: "ubuntu [version]", 26 | Short: "Fetch Vulnerability dictionary from Ubuntu", 27 | Long: `Fetch Vulnerability dictionary from Ubuntu`, 28 | Args: cobra.MinimumNArgs(1), 29 | RunE: fetchUbuntu, 30 | Example: "$ goval-dictionary fetch ubuntu 20.04 22.04", 31 | } 32 | 33 | func init() { 34 | fetchCmd.AddCommand(fetchUbuntuCmd) 35 | } 36 | 37 | func fetchUbuntu(_ *cobra.Command, args []string) (err error) { 38 | if err := log.SetLogger(viper.GetBool("log-to-file"), viper.GetString("log-dir"), viper.GetBool("debug"), viper.GetBool("log-json")); err != nil { 39 | return xerrors.Errorf("Failed to SetLogger. err: %w", err) 40 | } 41 | 42 | driver, err := db.NewDB(viper.GetString("dbtype"), viper.GetString("dbpath"), viper.GetBool("debug-sql"), db.Option{}) 43 | if err != nil { 44 | if errors.Is(err, db.ErrDBLocked) { 45 | return xerrors.Errorf("Failed to open DB. Close DB connection before fetching. err: %w", err) 46 | } 47 | return xerrors.Errorf("Failed to open DB. err: %w", err) 48 | } 49 | 50 | fetchMeta, err := driver.GetFetchMeta() 51 | if err != nil { 52 | return xerrors.Errorf("Failed to get FetchMeta from DB. err: %w", err) 53 | } 54 | if fetchMeta.OutDated() { 55 | return xerrors.Errorf("Failed to Insert CVEs into DB. SchemaVersion is old. SchemaVersion: %+v", map[string]uint{"latest": models.LatestSchemaVersion, "DB": fetchMeta.SchemaVersion}) 56 | } 57 | // If the fetch fails the first time (without SchemaVersion), the DB needs to be cleaned every time, so insert SchemaVersion. 58 | if err := driver.UpsertFetchMeta(fetchMeta); err != nil { 59 | return xerrors.Errorf("Failed to upsert FetchMeta to DB. err: %w", err) 60 | } 61 | 62 | results, err := fetcher.FetchFiles(util.Unique(args)) 63 | if err != nil { 64 | return xerrors.Errorf("Failed to fetch files. err: %w", err) 65 | } 66 | 67 | for _, r := range results { 68 | ovalroot := ubuntu.Root{} 69 | if err = xml.Unmarshal(r.Body, &ovalroot); err != nil { 70 | return xerrors.Errorf("Failed to unmarshal xml. url: %s, err: %w", r.URL, err) 71 | } 72 | 73 | log15.Info("Fetched", "File", r.URL[strings.LastIndex(r.URL, "/")+1:], "Count", len(ovalroot.Definitions.Definitions), "Timestamp", ovalroot.Generator.Timestamp) 74 | ts, err := time.Parse("2006-01-02T15:04:05", ovalroot.Generator.Timestamp) 75 | if err != nil { 76 | return xerrors.Errorf("Failed to parse timestamp. url: %s, timestamp: %s, err: %w", r.URL, ovalroot.Generator.Timestamp, err) 77 | } 78 | if ts.Before(time.Now().AddDate(0, 0, -3)) { 79 | log15.Warn("The fetched OVAL has not been updated for 3 days, the OVAL URL may have changed, please register a GitHub issue.", "GitHub", "https://github.com/vulsio/goval-dictionary/issues", "OVAL", r.URL, "Timestamp", ovalroot.Generator.Timestamp) 80 | } 81 | 82 | defs, err := ubuntu.ConvertToModel(&ovalroot) 83 | if err != nil { 84 | return xerrors.Errorf("Failed to convert from OVAL to goval-dictionary model. err: %w", err) 85 | } 86 | root := models.Root{ 87 | Family: c.Ubuntu, 88 | OSVersion: r.Target, 89 | Definitions: defs, 90 | Timestamp: time.Now(), 91 | } 92 | 93 | if err := driver.InsertOval(&root); err != nil { 94 | return xerrors.Errorf("Failed to insert OVAL. err: %w", err) 95 | } 96 | log15.Info("Finish", "Updated", len(root.Definitions)) 97 | } 98 | 99 | fetchMeta.LastFetchedAt = time.Now() 100 | if err := driver.UpsertFetchMeta(fetchMeta); err != nil { 101 | return xerrors.Errorf("Failed to upsert FetchMeta to DB. err: %w", err) 102 | } 103 | 104 | return nil 105 | } 106 | -------------------------------------------------------------------------------- /commands/fetch-debian.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "bytes" 5 | "encoding/xml" 6 | "errors" 7 | "strings" 8 | "time" 9 | 10 | "github.com/inconshreveable/log15" 11 | "github.com/spf13/cobra" 12 | "github.com/spf13/viper" 13 | "golang.org/x/net/html/charset" 14 | "golang.org/x/xerrors" 15 | 16 | c "github.com/vulsio/goval-dictionary/config" 17 | "github.com/vulsio/goval-dictionary/db" 18 | fetcher "github.com/vulsio/goval-dictionary/fetcher/debian" 19 | "github.com/vulsio/goval-dictionary/log" 20 | "github.com/vulsio/goval-dictionary/models" 21 | "github.com/vulsio/goval-dictionary/models/debian" 22 | "github.com/vulsio/goval-dictionary/util" 23 | ) 24 | 25 | // fetchDebianCmd is Subcommand for fetch Debian OVAL 26 | var fetchDebianCmd = &cobra.Command{ 27 | Use: "debian [version]", 28 | Short: "Fetch Vulnerability dictionary from Debian", 29 | Long: `Fetch Vulnerability dictionary from Debian`, 30 | Args: cobra.MinimumNArgs(1), 31 | RunE: fetchDebian, 32 | Example: "$ goval-dictionary fetch debian 10 11", 33 | } 34 | 35 | func init() { 36 | fetchCmd.AddCommand(fetchDebianCmd) 37 | } 38 | 39 | func fetchDebian(_ *cobra.Command, args []string) (err error) { 40 | if err := log.SetLogger(viper.GetBool("log-to-file"), viper.GetString("log-dir"), viper.GetBool("debug"), viper.GetBool("log-json")); err != nil { 41 | return xerrors.Errorf("Failed to SetLogger. err: %w", err) 42 | } 43 | 44 | driver, err := db.NewDB(viper.GetString("dbtype"), viper.GetString("dbpath"), viper.GetBool("debug-sql"), db.Option{}) 45 | if err != nil { 46 | if errors.Is(err, db.ErrDBLocked) { 47 | return xerrors.Errorf("Failed to open DB. Close DB connection before fetching. err: %w", err) 48 | } 49 | return xerrors.Errorf("Failed to open DB. err: %w", err) 50 | } 51 | 52 | fetchMeta, err := driver.GetFetchMeta() 53 | if err != nil { 54 | return xerrors.Errorf("Failed to get FetchMeta from DB. err: %w", err) 55 | } 56 | if fetchMeta.OutDated() { 57 | return xerrors.Errorf("Failed to Insert CVEs into DB. SchemaVersion is old. SchemaVersion: %+v", map[string]uint{"latest": models.LatestSchemaVersion, "DB": fetchMeta.SchemaVersion}) 58 | } 59 | // If the fetch fails the first time (without SchemaVersion), the DB needs to be cleaned every time, so insert SchemaVersion. 60 | if err := driver.UpsertFetchMeta(fetchMeta); err != nil { 61 | return xerrors.Errorf("Failed to upsert FetchMeta to DB. err: %w", err) 62 | } 63 | 64 | results, err := fetcher.FetchFiles(util.Unique(args)) 65 | if err != nil { 66 | return xerrors.Errorf("Failed to fetch files. err: %w", err) 67 | } 68 | 69 | for _, r := range results { 70 | ovalroot := debian.Root{} 71 | 72 | decoder := xml.NewDecoder(bytes.NewReader(r.Body)) 73 | decoder.CharsetReader = charset.NewReaderLabel 74 | if err := decoder.Decode(&ovalroot); err != nil { 75 | return xerrors.Errorf("Failed to unmarshal xml. url: %s, err: %w", r.URL, err) 76 | } 77 | 78 | log15.Info("Fetched", "File", r.URL[strings.LastIndex(r.URL, "/")+1:], "Count", len(ovalroot.Definitions.Definitions), "Timestamp", ovalroot.Generator.Timestamp) 79 | ts, err := time.Parse("2006-01-02T15:04:05.999-07:00", ovalroot.Generator.Timestamp) 80 | if err != nil { 81 | return xerrors.Errorf("Failed to parse timestamp. url: %s, timestamp: %s, err: %w", r.URL, ovalroot.Generator.Timestamp, err) 82 | } 83 | if ts.Before(time.Now().AddDate(0, 0, -3)) { 84 | log15.Warn("The fetched OVAL has not been updated for 3 days, the OVAL URL may have changed, please register a GitHub issue.", "GitHub", "https://github.com/vulsio/goval-dictionary/issues", "OVAL", r.URL, "Timestamp", ovalroot.Generator.Timestamp) 85 | } 86 | 87 | root := models.Root{ 88 | Family: c.Debian, 89 | OSVersion: r.Target, 90 | Definitions: debian.ConvertToModel(&ovalroot), 91 | Timestamp: time.Now(), 92 | } 93 | 94 | if err := driver.InsertOval(&root); err != nil { 95 | return xerrors.Errorf("Failed to insert OVAL. err: %w", err) 96 | } 97 | log15.Info("Finish", "Updated", len(root.Definitions)) 98 | } 99 | 100 | fetchMeta.LastFetchedAt = time.Now() 101 | if err := driver.UpsertFetchMeta(fetchMeta); err != nil { 102 | return xerrors.Errorf("Failed to upsert FetchMeta to DB. err: %w", err) 103 | } 104 | 105 | return nil 106 | } 107 | -------------------------------------------------------------------------------- /commands/fetch-oracle.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "encoding/xml" 5 | "errors" 6 | "slices" 7 | "strings" 8 | "time" 9 | 10 | "github.com/inconshreveable/log15" 11 | "github.com/spf13/cobra" 12 | "github.com/spf13/viper" 13 | "golang.org/x/xerrors" 14 | 15 | c "github.com/vulsio/goval-dictionary/config" 16 | "github.com/vulsio/goval-dictionary/db" 17 | fetcher "github.com/vulsio/goval-dictionary/fetcher/oracle" 18 | "github.com/vulsio/goval-dictionary/log" 19 | "github.com/vulsio/goval-dictionary/models" 20 | "github.com/vulsio/goval-dictionary/models/oracle" 21 | ) 22 | 23 | // fetchOracleCmd is Subcommand for fetch Oracle OVAL 24 | var fetchOracleCmd = &cobra.Command{ 25 | Use: "oracle [version]", 26 | Short: "Fetch Vulnerability dictionary from Oracle", 27 | Long: `Fetch Vulnerability dictionary from Oracle`, 28 | Args: cobra.MinimumNArgs(1), 29 | RunE: fetchOracle, 30 | Example: "$ goval-dictionary fetch oracle 8 9", 31 | } 32 | 33 | func init() { 34 | fetchCmd.AddCommand(fetchOracleCmd) 35 | } 36 | 37 | func fetchOracle(_ *cobra.Command, args []string) (err error) { 38 | if err := log.SetLogger(viper.GetBool("log-to-file"), viper.GetString("log-dir"), viper.GetBool("debug"), viper.GetBool("log-json")); err != nil { 39 | return xerrors.Errorf("Failed to SetLogger. err: %w", err) 40 | } 41 | 42 | driver, err := db.NewDB(viper.GetString("dbtype"), viper.GetString("dbpath"), viper.GetBool("debug-sql"), db.Option{}) 43 | if err != nil { 44 | if errors.Is(err, db.ErrDBLocked) { 45 | return xerrors.Errorf("Failed to open DB. Close DB connection before fetching. err: %w", err) 46 | } 47 | return xerrors.Errorf("Failed to open DB. err: %w", err) 48 | } 49 | 50 | fetchMeta, err := driver.GetFetchMeta() 51 | if err != nil { 52 | return xerrors.Errorf("Failed to get FetchMeta from DB. err: %w", err) 53 | } 54 | if fetchMeta.OutDated() { 55 | return xerrors.Errorf("Failed to Insert CVEs into DB. SchemaVersion is old. SchemaVersion: %+v", map[string]uint{"latest": models.LatestSchemaVersion, "DB": fetchMeta.SchemaVersion}) 56 | } 57 | // If the fetch fails the first time (without SchemaVersion), the DB needs to be cleaned every time, so insert SchemaVersion. 58 | if err := driver.UpsertFetchMeta(fetchMeta); err != nil { 59 | return xerrors.Errorf("Failed to upsert FetchMeta to DB. err: %w", err) 60 | } 61 | 62 | results, err := fetcher.FetchFiles() 63 | if err != nil { 64 | return xerrors.Errorf("Failed to fetch files. err: %w", err) 65 | } 66 | 67 | osVerDefs := map[string][]models.Definition{} 68 | for _, r := range results { 69 | ovalroot := oracle.Root{} 70 | if err = xml.Unmarshal(r.Body, &ovalroot); err != nil { 71 | return xerrors.Errorf("Failed to unmarshal xml. url: %s, err: %w", r.URL, err) 72 | } 73 | log15.Info("Fetched", "File", r.URL[strings.LastIndex(r.URL, "/")+1:], "Count", len(ovalroot.Definitions.Definitions), "Timestamp", ovalroot.Generator.Timestamp) 74 | ts, err := time.Parse("2006-01-02T15:04:05", ovalroot.Generator.Timestamp) 75 | if err != nil { 76 | return xerrors.Errorf("Failed to parse timestamp. url: %s, timestamp: %s, err: %w", r.URL, ovalroot.Generator.Timestamp, err) 77 | } 78 | if ts.Before(time.Now().AddDate(0, 0, -3)) { 79 | log15.Warn("The fetched OVAL has not been updated for 3 days, the OVAL URL may have changed, please register a GitHub issue.", "GitHub", "https://github.com/vulsio/goval-dictionary/issues", "OVAL", r.URL, "Timestamp", ovalroot.Generator.Timestamp) 80 | } 81 | 82 | for osVer, defs := range oracle.ConvertToModel(&ovalroot) { 83 | if slices.Contains(args, osVer) { 84 | osVerDefs[osVer] = append(osVerDefs[osVer], defs...) 85 | } 86 | } 87 | } 88 | 89 | for osVer, defs := range osVerDefs { 90 | root := models.Root{ 91 | Family: c.Oracle, 92 | OSVersion: osVer, 93 | Definitions: defs, 94 | Timestamp: time.Now(), 95 | } 96 | 97 | if err := driver.InsertOval(&root); err != nil { 98 | return xerrors.Errorf("Failed to insert OVAL. err: %w", err) 99 | } 100 | log15.Info("Finish", "Updated", len(root.Definitions)) 101 | } 102 | 103 | fetchMeta.LastFetchedAt = time.Now() 104 | if err := driver.UpsertFetchMeta(fetchMeta); err != nil { 105 | return xerrors.Errorf("Failed to upsert FetchMeta to DB. err: %w", err) 106 | } 107 | 108 | return nil 109 | } 110 | -------------------------------------------------------------------------------- /models/oracle/oracle.go: -------------------------------------------------------------------------------- 1 | package oracle 2 | 3 | import ( 4 | "strings" 5 | "time" 6 | 7 | "github.com/spf13/viper" 8 | 9 | "github.com/vulsio/goval-dictionary/models" 10 | ) 11 | 12 | type distroPackage struct { 13 | osVer string 14 | pack models.Package 15 | } 16 | 17 | // ConvertToModel Convert OVAL to models 18 | func ConvertToModel(root *Root) (defs map[string][]models.Definition) { 19 | osVerDefs := map[string][]models.Definition{} 20 | for _, ovaldef := range root.Definitions.Definitions { 21 | if strings.Contains(ovaldef.Description, "** REJECT **") { 22 | continue 23 | } 24 | 25 | cves := []models.Cve{} 26 | for _, c := range ovaldef.Advisory.Cves { 27 | cves = append(cves, models.Cve{ 28 | CveID: c.CveID, 29 | Href: c.Href, 30 | }) 31 | } 32 | 33 | rs := []models.Reference{} 34 | for _, r := range ovaldef.References { 35 | rs = append(rs, models.Reference{ 36 | Source: r.Source, 37 | RefID: r.RefID, 38 | RefURL: r.RefURL, 39 | }) 40 | } 41 | 42 | osVerPacks := map[string][]models.Package{} 43 | for _, distPack := range collectOraclePacks(ovaldef.Criteria) { 44 | osVerPacks[distPack.osVer] = append(osVerPacks[distPack.osVer], distPack.pack) 45 | } 46 | 47 | for osVer, packs := range osVerPacks { 48 | def := models.Definition{ 49 | DefinitionID: ovaldef.ID, 50 | Title: strings.TrimSpace(ovaldef.Title), 51 | Description: strings.TrimSpace(ovaldef.Description), 52 | Advisory: models.Advisory{ 53 | Severity: ovaldef.Advisory.Severity, 54 | Cves: append([]models.Cve{}, cves...), // If the same slice is used, it will only be stored once in the DB 55 | Bugzillas: []models.Bugzilla{}, 56 | AffectedCPEList: []models.Cpe{}, 57 | Issued: time.Date(1000, time.January, 1, 0, 0, 0, 0, time.UTC), 58 | Updated: time.Date(1000, time.January, 1, 0, 0, 0, 0, time.UTC), 59 | }, 60 | Debian: nil, 61 | AffectedPacks: append([]models.Package{}, packs...), // If the same slice is used, it will only be stored once in the DB 62 | References: append([]models.Reference{}, rs...), // If the same slice is used, it will only be stored once in the DB 63 | } 64 | 65 | if viper.GetBool("no-details") { 66 | def.Title = "" 67 | def.Description = "" 68 | def.Advisory.Severity = "" 69 | def.Advisory.Bugzillas = []models.Bugzilla{} 70 | def.Advisory.AffectedCPEList = []models.Cpe{} 71 | def.Advisory.Issued = time.Time{} 72 | def.Advisory.Updated = time.Time{} 73 | def.References = []models.Reference{} 74 | } 75 | 76 | osVerDefs[osVer] = append(osVerDefs[osVer], def) 77 | } 78 | } 79 | 80 | return osVerDefs 81 | } 82 | 83 | func collectOraclePacks(cri Criteria) []distroPackage { 84 | return walkOracle(cri, "", "", "", []distroPackage{}) 85 | } 86 | 87 | func walkOracle(cri Criteria, osVer, arch, label string, acc []distroPackage) []distroPackage { 88 | for _, c := range cri.Criterions { 89 | switch { 90 | case strings.HasPrefix(c.Comment, "Oracle Linux ") && strings.HasSuffix(c.Comment, " is installed"): // 91 | osVer = strings.TrimSuffix(strings.TrimPrefix(c.Comment, "Oracle Linux "), " is installed") 92 | case strings.HasPrefix(c.Comment, "Oracle Linux arch is "): // 93 | arch = strings.TrimSpace(strings.TrimPrefix(c.Comment, "Oracle Linux arch is ")) 94 | case strings.HasPrefix(c.Comment, "Module ") && strings.HasSuffix(c.Comment, " is enabled"): // 95 | label = strings.TrimSuffix(strings.TrimPrefix(c.Comment, "Module "), " is enabled") 96 | default: // , 97 | name, evr, ok := strings.Cut(c.Comment, " is earlier than ") 98 | if !ok { 99 | break 100 | } 101 | acc = append(acc, distroPackage{ 102 | osVer: osVer, 103 | pack: models.Package{ 104 | Name: name, 105 | Version: evr, 106 | Arch: arch, 107 | ModularityLabel: label, 108 | }, 109 | }) 110 | } 111 | } 112 | 113 | if len(cri.Criterias) == 0 { 114 | return acc 115 | } 116 | for _, c := range cri.Criterias { 117 | acc = walkOracle(c, osVer, arch, label, acc) 118 | } 119 | return acc 120 | } 121 | -------------------------------------------------------------------------------- /commands/fetch-redhat.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "encoding/xml" 5 | "errors" 6 | "fmt" 7 | "strings" 8 | "time" 9 | 10 | "github.com/inconshreveable/log15" 11 | "github.com/spf13/cobra" 12 | "github.com/spf13/viper" 13 | "golang.org/x/xerrors" 14 | 15 | c "github.com/vulsio/goval-dictionary/config" 16 | "github.com/vulsio/goval-dictionary/db" 17 | fetcher "github.com/vulsio/goval-dictionary/fetcher/redhat" 18 | "github.com/vulsio/goval-dictionary/log" 19 | "github.com/vulsio/goval-dictionary/models" 20 | "github.com/vulsio/goval-dictionary/models/redhat" 21 | "github.com/vulsio/goval-dictionary/util" 22 | ) 23 | 24 | // fetchRedHatCmd is Subcommand for fetch RedHat OVAL 25 | var fetchRedHatCmd = &cobra.Command{ 26 | Use: "redhat [version]", 27 | Short: "Fetch Vulnerability dictionary from RedHat", 28 | Long: `Fetch Vulnerability dictionary from RedHat`, 29 | Args: cobra.MinimumNArgs(1), 30 | RunE: fetchRedHat, 31 | Example: "$ goval-dictionary fetch redhat 8 9", 32 | } 33 | 34 | func init() { 35 | fetchCmd.AddCommand(fetchRedHatCmd) 36 | } 37 | 38 | func fetchRedHat(_ *cobra.Command, args []string) (err error) { 39 | if err := log.SetLogger(viper.GetBool("log-to-file"), viper.GetString("log-dir"), viper.GetBool("debug"), viper.GetBool("log-json")); err != nil { 40 | return xerrors.Errorf("Failed to SetLogger. err: %w", err) 41 | } 42 | 43 | driver, err := db.NewDB(viper.GetString("dbtype"), viper.GetString("dbpath"), viper.GetBool("debug-sql"), db.Option{}) 44 | if err != nil { 45 | if errors.Is(err, db.ErrDBLocked) { 46 | return xerrors.Errorf("Failed to open DB. Close DB connection before fetching. err: %w", err) 47 | } 48 | return xerrors.Errorf("Failed to open DB. err: %w", err) 49 | } 50 | 51 | fetchMeta, err := driver.GetFetchMeta() 52 | if err != nil { 53 | return xerrors.Errorf("Failed to get FetchMeta from DB. err: %w", err) 54 | } 55 | if fetchMeta.OutDated() { 56 | return xerrors.Errorf("Failed to Insert CVEs into DB. SchemaVersion is old. SchemaVersion: %+v", map[string]uint{"latest": models.LatestSchemaVersion, "DB": fetchMeta.SchemaVersion}) 57 | } 58 | // If the fetch fails the first time (without SchemaVersion), the DB needs to be cleaned every time, so insert SchemaVersion. 59 | if err := driver.UpsertFetchMeta(fetchMeta); err != nil { 60 | return xerrors.Errorf("Failed to upsert FetchMeta to DB. err: %w", err) 61 | } 62 | 63 | results, err := fetcher.FetchFiles(util.Unique(args)) 64 | if err != nil { 65 | return xerrors.Errorf("Failed to fetch files. err: %w", err) 66 | } 67 | 68 | for v, rs := range results { 69 | m := map[string]redhat.Root{} 70 | for _, r := range rs { 71 | ovalroot := redhat.Root{} 72 | if err = xml.Unmarshal(r.Body, &ovalroot); err != nil { 73 | return xerrors.Errorf("Failed to unmarshal xml. url: %s, err: %w", r.URL, err) 74 | } 75 | 76 | log15.Info("Fetched", "File", r.URL[strings.LastIndex(r.URL, "/")+1:], "Count", len(ovalroot.Definitions.Definitions), "Timestamp", ovalroot.Generator.Timestamp) 77 | ts, err := time.Parse("2006-01-02T15:04:05", ovalroot.Generator.Timestamp) 78 | if err != nil { 79 | return xerrors.Errorf("Failed to parse timestamp. url: %s, timestamp: %s, err: %w", r.URL, ovalroot.Generator.Timestamp, err) 80 | } 81 | if ts.Before(time.Now().AddDate(0, 0, -3)) { 82 | log15.Warn("The fetched OVAL has not been updated for 3 days, the OVAL URL may have changed, please register a GitHub issue.", "GitHub", "https://github.com/vulsio/goval-dictionary/issues", "OVAL", r.URL, "Timestamp", ovalroot.Generator.Timestamp) 83 | } 84 | 85 | m[r.URL[strings.LastIndex(r.URL, "/")+1:]] = ovalroot 86 | } 87 | 88 | roots := make([]redhat.Root, 0, len(m)) 89 | for _, k := range []string{fmt.Sprintf("rhel-%s-including-unpatched.oval.xml.bz2", v), fmt.Sprintf("rhel-%s-extras-including-unpatched.oval.xml.bz2", v), fmt.Sprintf("rhel-%s-supplementary.oval.xml.bz2", v), fmt.Sprintf("rhel-%s-els.oval.xml.bz2", v), fmt.Sprintf("com.redhat.rhsa-RHEL%s.xml", v), fmt.Sprintf("com.redhat.rhsa-RHEL%s-ELS.xml", v)} { 90 | roots = append(roots, m[k]) 91 | } 92 | 93 | root := models.Root{ 94 | Family: c.RedHat, 95 | OSVersion: v, 96 | Definitions: redhat.ConvertToModel(v, roots), 97 | Timestamp: time.Now(), 98 | } 99 | 100 | if err := driver.InsertOval(&root); err != nil { 101 | return xerrors.Errorf("Failed to insert OVAL. err: %w", err) 102 | } 103 | log15.Info("Finish", "Updated", len(root.Definitions)) 104 | } 105 | 106 | fetchMeta.LastFetchedAt = time.Now() 107 | if err := driver.UpsertFetchMeta(fetchMeta); err != nil { 108 | return xerrors.Errorf("Failed to upsert FetchMeta to DB. err: %w", err) 109 | } 110 | 111 | return nil 112 | } 113 | -------------------------------------------------------------------------------- /server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/url" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | 11 | "github.com/inconshreveable/log15" 12 | "github.com/labstack/echo/v4" 13 | "github.com/labstack/echo/v4/middleware" 14 | "github.com/spf13/viper" 15 | "golang.org/x/xerrors" 16 | 17 | "github.com/vulsio/goval-dictionary/db" 18 | ) 19 | 20 | // Start starts CVE dictionary HTTP Server. 21 | func Start(logToFile bool, logDir string, driver db.DB) error { 22 | e := echo.New() 23 | e.Debug = viper.GetBool("debug") 24 | 25 | // Middleware 26 | e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{Output: os.Stderr})) 27 | e.Use(middleware.Recover()) 28 | 29 | // setup access logger 30 | if logToFile { 31 | logPath := filepath.Join(logDir, "access.log") 32 | f, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) 33 | if err != nil { 34 | return xerrors.Errorf("Failed to open a log file. err: %w", err) 35 | } 36 | defer f.Close() 37 | e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{Output: f})) 38 | } 39 | 40 | // Routes 41 | e.GET("/health", health()) 42 | e.GET("/packs/:family/:release/:pack/:arch", getByPackName(driver)) 43 | e.GET("/packs/:family/:release/:pack", getByPackName(driver)) 44 | e.GET("/cves/:family/:release/:id/:arch", getByCveID(driver)) 45 | e.GET("/cves/:family/:release/:id", getByCveID(driver)) 46 | e.GET("/advisories/:family/:release", getAdvisories(driver)) 47 | e.GET("/count/:family/:release", countOvalDefs(driver)) 48 | e.GET("/lastmodified/:family/:release", getLastModified(driver)) 49 | // e.Post("/cpes", getByPackName(driver)) 50 | 51 | bindURL := fmt.Sprintf("%s:%s", viper.GetString("bind"), viper.GetString("port")) 52 | log15.Info("Listening...", "URL", bindURL) 53 | return e.Start(bindURL) 54 | } 55 | 56 | // Handler 57 | func health() echo.HandlerFunc { 58 | return func(c echo.Context) error { 59 | return c.String(http.StatusOK, "") 60 | } 61 | } 62 | 63 | func getByPackName(driver db.DB) echo.HandlerFunc { 64 | return func(c echo.Context) (err error) { 65 | family := strings.ToLower(c.Param("family")) 66 | release := c.Param("release") 67 | pack := c.Param("pack") 68 | arch := c.Param("arch") 69 | decodePack, err := url.QueryUnescape(pack) 70 | if err != nil { 71 | log15.Error(fmt.Sprintf("Failed to Decode Package Name: %s", err)) 72 | return c.JSON(http.StatusBadRequest, nil) 73 | } 74 | 75 | log15.Debug("Params", "Family", family, "Release", release, "Pack", pack, "DecodePack", decodePack, "arch", arch) 76 | 77 | defs, err := driver.GetByPackName(family, release, decodePack, arch) 78 | if err != nil { 79 | log15.Error("Failed to get by Package Name.", "err", err) 80 | } 81 | return c.JSON(http.StatusOK, defs) 82 | } 83 | } 84 | 85 | func getByCveID(driver db.DB) echo.HandlerFunc { 86 | return func(c echo.Context) (err error) { 87 | family := strings.ToLower(c.Param("family")) 88 | release := c.Param("release") 89 | cveID := c.Param("id") 90 | arch := c.Param("arch") 91 | log15.Debug("Params", "Family", family, "Release", release, "CveID", cveID, "arch", arch) 92 | 93 | defs, err := driver.GetByCveID(family, release, cveID, arch) 94 | if err != nil { 95 | log15.Error("Failed to get by CveID.", "err", err) 96 | } 97 | return c.JSON(http.StatusOK, defs) 98 | } 99 | } 100 | 101 | func getAdvisories(driver db.DB) echo.HandlerFunc { 102 | return func(c echo.Context) (err error) { 103 | family := strings.ToLower(c.Param("family")) 104 | release := c.Param("release") 105 | log15.Debug("Params", "Family", family, "Release", release) 106 | 107 | m, err := driver.GetAdvisories(family, release) 108 | if err != nil { 109 | log15.Error("Failed to get advisories.", "err", err) 110 | } 111 | return c.JSON(http.StatusOK, m) 112 | } 113 | } 114 | 115 | func countOvalDefs(driver db.DB) echo.HandlerFunc { 116 | return func(c echo.Context) (err error) { 117 | family := strings.ToLower(c.Param("family")) 118 | release := c.Param("release") 119 | log15.Debug("Params", "Family", family, "Release", release) 120 | 121 | count, err := driver.CountDefs(family, release) 122 | if err != nil { 123 | log15.Error("Failed to count OVAL defs.", "err", err) 124 | } 125 | return c.JSON(http.StatusOK, count) 126 | } 127 | } 128 | 129 | func getLastModified(driver db.DB) echo.HandlerFunc { 130 | return func(c echo.Context) (err error) { 131 | family := strings.ToLower(c.Param("family")) 132 | release := c.Param("release") 133 | log15.Debug("Params", "Family", family, "Release", release) 134 | 135 | t, err := driver.GetLastModified(family, release) 136 | if err != nil { 137 | log15.Error(fmt.Sprintf("Failed to GetLastModified: %s", err)) 138 | return c.JSON(http.StatusInternalServerError, nil) 139 | } 140 | 141 | return c.JSON(http.StatusOK, t) 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /models/debian/debian_test.go: -------------------------------------------------------------------------------- 1 | package debian 2 | 3 | import ( 4 | "encoding/xml" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/k0kubun/pp" 9 | 10 | "github.com/vulsio/goval-dictionary/models" 11 | ) 12 | 13 | func TestWalkDebian(t *testing.T) { 14 | var tests = []struct { 15 | oval string 16 | expected []models.Package 17 | }{ 18 | { 19 | oval: ` 20 | 21 | 22 | 23 | Debian 24 | 5.3 25 | 2017-04-07T03:47:55.188-04:00 26 | 27 | 28 | 29 | 30 | CVE-2014-0001 31 | 32 | Debian GNU/Linux 7.0 33 | Debian GNU/Linux 8.2 34 | Debian GNU/Linux 9.0 35 | mysql-5.5 36 | 37 | 38 | Buffer overflow in client/mysql.cc in Oracle MySQL and MariaDB before 5.5.35 allows remote database servers to cause a denial of service (crash) and possibly execute arbitrary code via a long server version string. 39 | 40 | 2014-05-03 41 | 42 | DSA-2919 43 | Several issues have been discovered in the MySQL database server. The 44 | vulnerabilities are addressed by upgrading MySQL to the new upstream 45 | version 5.5.37. Please see the MySQL 5.5 Release Notes and Oracle's 46 | Critical Patch Update advisory for further details: 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | `, 89 | expected: []models.Package{ 90 | { 91 | Name: "mysql-5.5", 92 | Version: "5.5.37-0+wheezy1", 93 | }, 94 | { 95 | Name: "mysql-5.5", 96 | Version: "5.5.37-1", 97 | }, 98 | { 99 | Name: "mysql-5.5", 100 | Version: "5.5.37-1", 101 | }, 102 | { 103 | Name: "mysql-5.6", 104 | Version: "5.6.37-1", 105 | }, 106 | }, 107 | }, 108 | } 109 | 110 | for i, tt := range tests { 111 | var root *Root 112 | if err := xml.Unmarshal([]byte(tt.oval), &root); err != nil { 113 | t.Errorf("[%d] marshall error", i) 114 | } 115 | c := root.Definitions.Definitions[0].Criteria 116 | actual := collectDebianPacks(c) 117 | 118 | if !reflect.DeepEqual(tt.expected, actual) { 119 | e := pp.Sprintf("%v", tt.expected) 120 | a := pp.Sprintf("%v", actual) 121 | t.Errorf("[%d]: expected: %s\n, actual: %s\n", i, e, a) 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /models/oracle/types.go: -------------------------------------------------------------------------------- 1 | package oracle 2 | 3 | import "encoding/xml" 4 | 5 | // Root : root object 6 | type Root struct { 7 | XMLName xml.Name `xml:"oval_definitions"` 8 | Generator Generator `xml:"generator"` 9 | Definitions Definitions `xml:"definitions"` 10 | Tests Tests `xml:"tests"` 11 | Objects Objects `xml:"objects"` 12 | States States `xml:"states"` 13 | } 14 | 15 | // Generator : >generator 16 | type Generator struct { 17 | XMLName xml.Name `xml:"generator"` 18 | ProductName string `xml:"product_name"` 19 | ProductVersion string `xml:"product_version"` 20 | SchemaVersion string `xml:"schema_version"` 21 | Timestamp string `xml:"timestamp"` 22 | } 23 | 24 | // Definitions : >definitions 25 | type Definitions struct { 26 | XMLName xml.Name `xml:"definitions"` 27 | Definitions []Definition `xml:"definition"` 28 | } 29 | 30 | // Definition : >definitions>definition 31 | type Definition struct { 32 | XMLName xml.Name `xml:"definition"` 33 | ID string `xml:"id,attr"` 34 | Class string `xml:"class,attr"` 35 | Title string `xml:"metadata>title"` 36 | Affecteds []Affected `xml:"metadata>affected"` 37 | References []Reference `xml:"metadata>reference"` 38 | Description string `xml:"metadata>description"` 39 | Advisory Advisory `xml:"metadata>advisory"` 40 | Criteria Criteria `xml:"criteria"` 41 | } 42 | 43 | // Criteria : >definitions>definition>criteria 44 | type Criteria struct { 45 | XMLName xml.Name `xml:"criteria"` 46 | Operator string `xml:"operator,attr"` 47 | Criterias []Criteria `xml:"criteria"` 48 | Criterions []Criterion `xml:"criterion"` 49 | } 50 | 51 | // Criterion : >definitions>definition>criteria>*>criterion 52 | type Criterion struct { 53 | XMLName xml.Name `xml:"criterion"` 54 | TestRef string `xml:"test_ref,attr"` 55 | Comment string `xml:"comment,attr"` 56 | } 57 | 58 | // Affected : >definitions>definition>metadata>affected 59 | type Affected struct { 60 | XMLName xml.Name `xml:"affected"` 61 | Family string `xml:"family,attr"` 62 | Platforms []string `xml:"platform"` 63 | } 64 | 65 | // Reference : >definitions>definition>metadata>reference 66 | type Reference struct { 67 | XMLName xml.Name `xml:"reference"` 68 | Source string `xml:"source,attr"` 69 | RefID string `xml:"ref_id,attr"` 70 | RefURL string `xml:"ref_url,attr"` 71 | } 72 | 73 | // Advisory : >definitions>definition>metadata>advisory 74 | // RedHat and Ubuntu OVAL 75 | type Advisory struct { 76 | XMLName xml.Name `xml:"advisory"` 77 | Severity string `xml:"severity"` 78 | Rights string `xml:"rights"` 79 | Cves []Cve `xml:"cve"` 80 | Issued struct { 81 | Date string `xml:"date,attr"` 82 | } `xml:"issued"` 83 | } 84 | 85 | // Cve : >definitions>definition>metadata>advisory>cve 86 | // RedHat OVAL 87 | type Cve struct { 88 | XMLName xml.Name `xml:"cve"` 89 | CveID string `xml:",chardata"` 90 | Href string `xml:"href,attr"` 91 | } 92 | 93 | // Tests : >tests 94 | type Tests struct { 95 | XMLName xml.Name `xml:"tests"` 96 | RpminfoTest RpminfoTest `xml:"rpminfo_test"` 97 | } 98 | 99 | // RpminfoTest : >tests>rpminfo_test 100 | type RpminfoTest []struct { 101 | ID string `xml:"id,attr"` 102 | Comment string `xml:"comment,attr"` 103 | Check string `xml:"check,attr"` 104 | Object ObjectRef `xml:"object"` 105 | State StateRef `xml:"state"` 106 | } 107 | 108 | // ObjectRef : >tests>rpminfo_test>object-object_ref 109 | type ObjectRef struct { 110 | XMLName xml.Name `xml:"object"` 111 | Text string `xml:",chardata"` 112 | ObjectRef string `xml:"object_ref,attr"` 113 | } 114 | 115 | // StateRef : >tests>rpminfo_test>state-state_ref 116 | type StateRef struct { 117 | XMLName xml.Name `xml:"state"` 118 | Text string `xml:",chardata"` 119 | StateRef string `xml:"state_ref,attr"` 120 | } 121 | 122 | // Objects : >objects 123 | type Objects struct { 124 | XMLName xml.Name `xml:"objects"` 125 | RpminfoObject []RpminfoObject `xml:"rpminfo_object"` 126 | } 127 | 128 | // RpminfoObject : >objects>rpminfo_object 129 | type RpminfoObject struct { 130 | ID string `xml:"id,attr"` 131 | Name string `xml:"name"` 132 | } 133 | 134 | // States : >states 135 | type States struct { 136 | XMLName xml.Name `xml:"states"` 137 | RpminfoState []RpminfoState `xml:"rpminfo_state"` 138 | } 139 | 140 | // RpminfoState : >states>rpminfo_state 141 | type RpminfoState struct { 142 | ID string `xml:"id,attr"` 143 | SignatureKeyid struct { 144 | Text string `xml:",chardata"` 145 | Operation string `xml:"operation,attr"` 146 | } `xml:"signature_keyid"` 147 | Version struct { 148 | Text string `xml:",chardata"` 149 | Operation string `xml:"operation,attr"` 150 | } `xml:"version"` 151 | Arch struct { 152 | Text string `xml:",chardata"` 153 | Operation string `xml:"operation,attr"` 154 | } `xml:"arch"` 155 | Evr struct { 156 | Text string `xml:",chardata"` 157 | Datatype string `xml:"datatype,attr"` 158 | Operation string `xml:"operation,attr"` 159 | } `xml:"evr"` 160 | } 161 | -------------------------------------------------------------------------------- /fetcher/redhat/redhat.go: -------------------------------------------------------------------------------- 1 | package redhat 2 | 3 | import ( 4 | "archive/tar" 5 | "bytes" 6 | "fmt" 7 | "io" 8 | "slices" 9 | "strconv" 10 | "strings" 11 | 12 | "github.com/inconshreveable/log15" 13 | "golang.org/x/xerrors" 14 | 15 | "github.com/vulsio/goval-dictionary/fetcher/util" 16 | ) 17 | 18 | // FetchFiles fetch OVAL from RedHat 19 | func FetchFiles(versions []string) (map[string][]util.FetchResult, error) { 20 | results := map[string][]util.FetchResult{} 21 | for _, v := range versions { 22 | switch v { 23 | case "1", "2", "3": 24 | log15.Warn("Skip redhat because no vulnerability information provided.", "version", v) 25 | case "4": 26 | rs, err := fetchOVALv1([]string{fmt.Sprintf("com.redhat.rhsa-RHEL%s.xml", v)}) 27 | if err != nil { 28 | return nil, xerrors.Errorf("Failed to fetch OVALv1. err: %w", err) 29 | } 30 | results[v] = rs 31 | case "5": 32 | rs, err := fetchOVALv1([]string{fmt.Sprintf("com.redhat.rhsa-RHEL%s.xml", v), fmt.Sprintf("com.redhat.rhsa-RHEL%s-ELS.xml", v)}) 33 | if err != nil { 34 | return nil, xerrors.Errorf("Failed to fetch OVALv1. err: %w", err) 35 | } 36 | results[v] = rs 37 | case "6": 38 | rs, err := fetchOVALv1([]string{fmt.Sprintf("com.redhat.rhsa-RHEL%s.xml", v)}) 39 | if err != nil { 40 | return nil, xerrors.Errorf("Failed to fetch OVALv1. err: %w", err) 41 | } 42 | results[v] = rs 43 | 44 | rs, err = fetchOVALv2([]string{fmt.Sprintf("%s-including-unpatched", v), fmt.Sprintf("%s-extras-including-unpatched", v), fmt.Sprintf("%s-supplementary", v), fmt.Sprintf("%s-els", v)}) 45 | if err != nil { 46 | return nil, xerrors.Errorf("Failed to fetch OVALv2. err: %w", err) 47 | } 48 | results[v] = append(results[v], rs...) 49 | case "7": 50 | rs, err := fetchOVALv1([]string{fmt.Sprintf("com.redhat.rhsa-RHEL%s.xml", v)}) 51 | if err != nil { 52 | return nil, xerrors.Errorf("Failed to fetch OVALv1. err: %w", err) 53 | } 54 | results[v] = rs 55 | 56 | rs, err = fetchOVALv2([]string{fmt.Sprintf("%s-including-unpatched", v), fmt.Sprintf("%s-extras-including-unpatched", v), fmt.Sprintf("%s-supplementary", v)}) 57 | if err != nil { 58 | return nil, xerrors.Errorf("Failed to fetch OVALv2. err: %w", err) 59 | } 60 | results[v] = append(results[v], rs...) 61 | case "8", "9": 62 | rs, err := fetchOVALv1([]string{fmt.Sprintf("com.redhat.rhsa-RHEL%s.xml", v)}) 63 | if err != nil { 64 | return nil, xerrors.Errorf("Failed to fetch OVALv1. err: %w", err) 65 | } 66 | results[v] = rs 67 | 68 | rs, err = fetchOVALv2([]string{fmt.Sprintf("%s-including-unpatched", v)}) 69 | if err != nil { 70 | return nil, xerrors.Errorf("Failed to fetch OVALv2. err: %w", err) 71 | } 72 | results[v] = append(results[v], rs...) 73 | default: 74 | if _, err := strconv.Atoi(v); err != nil { 75 | log15.Warn("Skip unknown redhat.", "version", v) 76 | break 77 | } 78 | 79 | rs, err := fetchOVALv2([]string{fmt.Sprintf("%s-including-unpatched", v)}) 80 | if err != nil { 81 | return nil, xerrors.Errorf("Failed to fetch OVALv2. err: %w", err) 82 | } 83 | results[v] = rs 84 | } 85 | } 86 | 87 | if len(results) == 0 { 88 | return nil, xerrors.New("There are no versions to fetch") 89 | } 90 | return results, nil 91 | } 92 | 93 | func fetchOVALv1(names []string) ([]util.FetchResult, error) { 94 | rs, err := util.FetchFeedFiles([]util.FetchRequest{{ 95 | Target: "oval_v1_20230706.tar.gz", 96 | URL: "https://access.redhat.com/security/data/archive/oval_v1_20230706.tar.gz", 97 | MIMEType: util.MIMETypeGzip, 98 | }}) 99 | if err != nil { 100 | return nil, xerrors.Errorf("Failed to fetch. err: %w", err) 101 | } 102 | 103 | results := make([]util.FetchResult, 0, len(names)) 104 | 105 | tr := tar.NewReader(bytes.NewReader(rs[0].Body)) 106 | for { 107 | hdr, err := tr.Next() 108 | if err == io.EOF { 109 | break 110 | } 111 | if err != nil { 112 | return nil, xerrors.Errorf("Failed to next tar reader. err: %w", err) 113 | } 114 | 115 | if !slices.Contains(names, hdr.Name) { 116 | continue 117 | } 118 | 119 | bs, err := io.ReadAll(tr) 120 | if err != nil { 121 | return nil, xerrors.Errorf("Failed to read all %s. err: %w", hdr.Name, err) 122 | } 123 | results = append(results, util.FetchResult{ 124 | Target: strings.TrimSuffix(strings.TrimPrefix(hdr.Name, "com.redhat.rhsa-RHEL"), ".xml"), 125 | URL: fmt.Sprintf("https://access.redhat.com/security/data/archive/oval_v1_20230706.tar.gz/%s", hdr.Name), 126 | Body: bs, 127 | }) 128 | } 129 | 130 | return results, nil 131 | } 132 | 133 | func fetchOVALv2(names []string) ([]util.FetchResult, error) { 134 | reqs := make([]util.FetchRequest, 0, len(names)) 135 | for _, n := range names { 136 | reqs = append(reqs, util.FetchRequest{ 137 | Target: n, 138 | URL: fmt.Sprintf("https://access.redhat.com/security/data/oval/v2/RHEL%s/rhel-%s.oval.xml.bz2", strings.Split(n, "-")[0], n), 139 | MIMEType: util.MIMETypeBzip2, 140 | }) 141 | } 142 | 143 | rs, err := util.FetchFeedFiles(reqs) 144 | if err != nil { 145 | return nil, xerrors.Errorf("Failed to fetch. err: %w", err) 146 | } 147 | return rs, nil 148 | } 149 | -------------------------------------------------------------------------------- /commands/fetch-suse.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "encoding/xml" 5 | "errors" 6 | "strings" 7 | "time" 8 | 9 | "github.com/inconshreveable/log15" 10 | "github.com/spf13/cobra" 11 | "github.com/spf13/viper" 12 | "golang.org/x/xerrors" 13 | 14 | c "github.com/vulsio/goval-dictionary/config" 15 | "github.com/vulsio/goval-dictionary/db" 16 | fetcher "github.com/vulsio/goval-dictionary/fetcher/suse" 17 | "github.com/vulsio/goval-dictionary/log" 18 | "github.com/vulsio/goval-dictionary/models" 19 | "github.com/vulsio/goval-dictionary/models/suse" 20 | "github.com/vulsio/goval-dictionary/util" 21 | ) 22 | 23 | // fetchSUSECmd is Subcommand for fetch SUSE OVAL 24 | var fetchSUSECmd = &cobra.Command{ 25 | Use: "suse [version]", 26 | Short: "Fetch Vulnerability dictionary from SUSE", 27 | Long: `Fetch Vulnerability dictionary from SUSE`, 28 | RunE: fetchSUSE, 29 | Example: `$ goval-dictionary fetch suse --suse-type opensuse 13.2 tumbleweed 30 | $ goval-dictionary fetch suse --suse-type opensuse-leap 15.2 15.3 31 | $ goval-dictionary fetch suse --suse-type suse-enterprise-server 12 15 32 | $ goval-dictionary fetch suse --suse-type suse-enterprise-desktop 12 15`, 33 | } 34 | 35 | func init() { 36 | fetchCmd.AddCommand(fetchSUSECmd) 37 | 38 | fetchSUSECmd.PersistentFlags().String("suse-type", "opensuse-leap", "Fetch SUSE Type(choices: opensuse, opensuse-leap, suse-enterprise-server, suse-enterprise-desktop)") 39 | _ = viper.BindPFlag("suse-type", fetchSUSECmd.PersistentFlags().Lookup("suse-type")) 40 | } 41 | 42 | func fetchSUSE(_ *cobra.Command, args []string) (err error) { 43 | if err := log.SetLogger(viper.GetBool("log-to-file"), viper.GetString("log-dir"), viper.GetBool("debug"), viper.GetBool("log-json")); err != nil { 44 | return xerrors.Errorf("Failed to SetLogger. err: %w", err) 45 | } 46 | 47 | var suseType string 48 | switch viper.GetString("suse-type") { 49 | case "opensuse": 50 | suseType = c.OpenSUSE 51 | case "opensuse-leap": 52 | suseType = c.OpenSUSELeap 53 | case "suse-enterprise-server": 54 | suseType = c.SUSEEnterpriseServer 55 | case "suse-enterprise-desktop": 56 | suseType = c.SUSEEnterpriseDesktop 57 | default: 58 | return xerrors.Errorf("Specify SUSE type to fetch. Available SUSE Type: opensuse, opensuse-leap, suse-enterprise-server, suse-enterprise-desktop") 59 | } 60 | 61 | driver, err := db.NewDB(viper.GetString("dbtype"), viper.GetString("dbpath"), viper.GetBool("debug-sql"), db.Option{}) 62 | if err != nil { 63 | if errors.Is(err, db.ErrDBLocked) { 64 | return xerrors.Errorf("Failed to open DB. Close DB connection before fetching. err: %w", err) 65 | } 66 | return xerrors.Errorf("Failed to open DB. err: %w", err) 67 | } 68 | 69 | fetchMeta, err := driver.GetFetchMeta() 70 | if err != nil { 71 | return xerrors.Errorf("Failed to get FetchMeta from DB. err: %w", err) 72 | } 73 | if fetchMeta.OutDated() { 74 | return xerrors.Errorf("Failed to Insert CVEs into DB. SchemaVersion is old. SchemaVersion: %+v", map[string]uint{"latest": models.LatestSchemaVersion, "DB": fetchMeta.SchemaVersion}) 75 | } 76 | // If the fetch fails the first time (without SchemaVersion), the DB needs to be cleaned every time, so insert SchemaVersion. 77 | if err := driver.UpsertFetchMeta(fetchMeta); err != nil { 78 | return xerrors.Errorf("Failed to upsert FetchMeta to DB. err: %w", err) 79 | } 80 | 81 | results, err := fetcher.FetchFiles(suseType, util.Unique(args)) 82 | if err != nil { 83 | return xerrors.Errorf("Failed to fetch files. err: %w", err) 84 | } 85 | 86 | for _, r := range results { 87 | ovalroot := suse.Root{} 88 | if err = xml.Unmarshal(r.Body, &ovalroot); err != nil { 89 | return xerrors.Errorf("Failed to unmarshal xml. url: %s, err: %w", r.URL, err) 90 | } 91 | filename := r.URL[strings.LastIndex(r.URL, "/")+1:] 92 | log15.Info("Fetched", "File", filename, "Count", len(ovalroot.Definitions.Definitions), "Timestamp", ovalroot.Generator.Timestamp) 93 | ts, err := time.Parse("2006-01-02T15:04:05", ovalroot.Generator.Timestamp) 94 | if err != nil { 95 | return xerrors.Errorf("Failed to parse timestamp. url: %s, timestamp: %s, err: %w", r.URL, ovalroot.Generator.Timestamp, err) 96 | } 97 | if ts.Before(time.Now().AddDate(0, 0, -3)) { 98 | log15.Warn("The fetched OVAL has not been updated for 3 days, the OVAL URL may have changed, please register a GitHub issue.", "GitHub", "https://github.com/vulsio/goval-dictionary/issues", "OVAL", r.URL, "Timestamp", ovalroot.Generator.Timestamp) 99 | } 100 | 101 | osVerDefs, err := suse.ConvertToModel(filename, &ovalroot) 102 | if err != nil { 103 | return xerrors.Errorf("Failed to convert from OVAL to goval-dictionary model. err: %w", err) 104 | } 105 | for osVer, defs := range osVerDefs { 106 | root := models.Root{ 107 | Family: suseType, 108 | OSVersion: osVer, 109 | Definitions: defs, 110 | Timestamp: time.Now(), 111 | } 112 | if err := driver.InsertOval(&root); err != nil { 113 | return xerrors.Errorf("Failed to insert OVAL. err: %w", err) 114 | } 115 | log15.Info("Finish", "Updated", len(root.Definitions)) 116 | } 117 | } 118 | 119 | fetchMeta.LastFetchedAt = time.Now() 120 | if err := driver.UpsertFetchMeta(fetchMeta); err != nil { 121 | return xerrors.Errorf("Failed to upsert FetchMeta to DB. err: %w", err) 122 | } 123 | 124 | return nil 125 | } 126 | -------------------------------------------------------------------------------- /models/models.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "time" 5 | 6 | "gorm.io/gorm" 7 | ) 8 | 9 | // LatestSchemaVersion manages the Schema version used in the latest goval-dictionary. 10 | const LatestSchemaVersion = 2 11 | 12 | // FetchMeta has DB information 13 | type FetchMeta struct { 14 | gorm.Model `json:"-"` 15 | GovalDictRevision string 16 | SchemaVersion uint 17 | LastFetchedAt time.Time 18 | } 19 | 20 | // OutDated checks whether last fetched feed is out dated 21 | func (f FetchMeta) OutDated() bool { 22 | return f.SchemaVersion != LatestSchemaVersion 23 | } 24 | 25 | // Root is root struct 26 | type Root struct { 27 | ID uint `gorm:"primary_key"` 28 | Family string `gorm:"type:varchar(255)"` 29 | OSVersion string `gorm:"type:varchar(255)"` 30 | Definitions []Definition 31 | Timestamp time.Time 32 | } 33 | 34 | // Definition : >definitions>definition 35 | type Definition struct { 36 | ID uint `gorm:"primary_key" json:"-"` 37 | RootID uint `gorm:"index:idx_definition_root_id" json:"-" xml:"-"` 38 | 39 | DefinitionID string `gorm:"type:varchar(255)"` 40 | Title string `gorm:"type:text"` 41 | Description string // If the type:text, varchar(255) is specified, MySQL overflows and gives an error. No problem in GORMv2. (https://github.com/go-gorm/mysql/tree/15e2cbc6fd072be99215a82292e025dab25e2e16#configuration) 42 | Advisory Advisory 43 | Debian *Debian 44 | AffectedPacks []Package 45 | References []Reference 46 | } 47 | 48 | // Package affected 49 | type Package struct { 50 | ID uint `gorm:"primary_key" json:"-"` 51 | DefinitionID uint `gorm:"index:idx_packages_definition_id" json:"-" xml:"-"` 52 | 53 | Name string `gorm:"index:idx_packages_name"` // If the type:text, varchar(255) is specified, MySQL overflows and gives an error. No problem in GORMv2. (https://github.com/go-gorm/mysql/tree/15e2cbc6fd072be99215a82292e025dab25e2e16#configuration) 54 | Version string `gorm:"type:varchar(255)"` // affected earlier than this version 55 | Arch string `gorm:"type:varchar(255)"` // Used for Amazon Linux, Oracle Linux and Fedora 56 | NotFixedYet bool // Used for RedHat, Ubuntu 57 | ModularityLabel string `gorm:"type:varchar(255)"` // RHEL 8 or later only 58 | } 59 | 60 | // Reference : >definitions>definition>metadata>reference 61 | type Reference struct { 62 | ID uint `gorm:"primary_key" json:"-"` 63 | DefinitionID uint `gorm:"index:idx_reference_definition_id" json:"-" xml:"-"` 64 | 65 | Source string `gorm:"type:varchar(255)"` 66 | RefID string `gorm:"type:varchar(255)"` 67 | RefURL string `gorm:"type:text"` 68 | } 69 | 70 | // Advisory : >definitions>definition>metadata>advisory 71 | type Advisory struct { 72 | ID uint `gorm:"primary_key" json:"-"` 73 | DefinitionID uint `gorm:"index:idx_advisories_definition_id" json:"-" xml:"-"` 74 | 75 | Severity string `gorm:"type:varchar(255)"` 76 | Cves []Cve 77 | Bugzillas []Bugzilla 78 | AffectedResolution []Resolution 79 | AffectedCPEList []Cpe 80 | AffectedRepository string `gorm:"type:varchar(255)"` // Amazon Linux 2 Only 81 | Issued time.Time 82 | Updated time.Time 83 | } 84 | 85 | // Cve : >definitions>definition>metadata>advisory>cve 86 | type Cve struct { 87 | ID uint `gorm:"primary_key" json:"-"` 88 | AdvisoryID uint `gorm:"index:idx_cves_advisory_id" json:"-" xml:"-"` 89 | 90 | CveID string `gorm:"type:varchar(255)"` 91 | Cvss2 string `gorm:"type:varchar(255)"` 92 | Cvss3 string `gorm:"type:varchar(255)"` 93 | Cwe string `gorm:"type:varchar(255)"` 94 | Impact string `gorm:"type:varchar(255)"` 95 | Href string `gorm:"type:varchar(255)"` 96 | Public string `gorm:"type:varchar(255)"` 97 | } 98 | 99 | // Bugzilla : >definitions>definition>metadata>advisory>bugzilla 100 | type Bugzilla struct { 101 | ID uint `gorm:"primary_key" json:"-"` 102 | AdvisoryID uint `gorm:"index:idx_bugzillas_advisory_id" json:"-" xml:"-"` 103 | 104 | BugzillaID string `gorm:"type:varchar(255)"` 105 | URL string `gorm:"type:varchar(255)"` 106 | Title string `gorm:"type:varchar(255)"` 107 | } 108 | 109 | // Resolution : >definitions>definition>metadata>advisory>affected>resolution 110 | type Resolution struct { 111 | ID uint `gorm:"primary_key" json:"-"` 112 | AdvisoryID uint `gorm:"index:idx_resolution_advisory_id" json:"-" xml:"-"` 113 | 114 | State string `gorm:"type:varchar(255)"` 115 | Components []Component 116 | } 117 | 118 | // Component : >definitions>definition>metadata>advisory>affected>resolution>component 119 | type Component struct { 120 | ID uint `gorm:"primary_key" json:"-"` 121 | ResolutionID uint `gorm:"index:idx_component_resolution_id" json:"-" xml:"-"` 122 | 123 | Component string `gorm:"type:varchar(255)"` 124 | } 125 | 126 | // Cpe : >definitions>definition>metadata>advisory>affected_cpe_list 127 | type Cpe struct { 128 | ID uint `gorm:"primary_key" json:"-"` 129 | AdvisoryID uint `gorm:"index:idx_cpes_advisory_id" json:"-" xml:"-"` 130 | 131 | Cpe string `gorm:"type:varchar(255)"` 132 | } 133 | 134 | // Debian : >definitions>definition>metadata>debian 135 | type Debian struct { 136 | ID uint `gorm:"primary_key" json:"-"` 137 | DefinitionID uint `gorm:"index:idx_debian_definition_id" json:"-" xml:"-"` 138 | 139 | DSA string `gorm:"type:varchar(255)"` 140 | MoreInfo string `gorm:"type:text"` 141 | 142 | Date time.Time 143 | } 144 | -------------------------------------------------------------------------------- /models/suse/types.go: -------------------------------------------------------------------------------- 1 | package suse 2 | 3 | import "encoding/xml" 4 | 5 | // Root : root object 6 | type Root struct { 7 | XMLName xml.Name `xml:"oval_definitions"` 8 | Generator Generator `xml:"generator"` 9 | Definitions Definitions `xml:"definitions"` 10 | Tests Tests `xml:"tests"` 11 | Objects Objects `xml:"objects"` 12 | States States `xml:"states"` 13 | } 14 | 15 | // Generator : >generator 16 | type Generator struct { 17 | XMLName xml.Name `xml:"generator"` 18 | ProductName string `xml:"product_name"` 19 | SchemaVersion string `xml:"schema_version"` 20 | Timestamp string `xml:"timestamp"` 21 | } 22 | 23 | // Definitions : >definitions 24 | type Definitions struct { 25 | XMLName xml.Name `xml:"definitions"` 26 | Definitions []Definition `xml:"definition"` 27 | } 28 | 29 | // Definition : >definitions>definition 30 | type Definition struct { 31 | XMLName xml.Name `xml:"definition"` 32 | ID string `xml:"id,attr"` 33 | Class string `xml:"class,attr"` 34 | Title string `xml:"metadata>title"` 35 | Affecteds []Affected `xml:"metadata>affected"` 36 | References []Reference `xml:"metadata>reference"` 37 | Description string `xml:"metadata>description"` 38 | Advisory Advisory `xml:"metadata>advisory"` 39 | Criteria Criteria `xml:"criteria"` 40 | } 41 | 42 | // Criteria : >definitions>definition>criteria 43 | type Criteria struct { 44 | XMLName xml.Name `xml:"criteria"` 45 | Operator string `xml:"operator,attr"` 46 | Criterias []Criteria `xml:"criteria"` 47 | Criterions []Criterion `xml:"criterion"` 48 | } 49 | 50 | // Criterion : >definitions>definition>criteria>*>criterion 51 | type Criterion struct { 52 | XMLName xml.Name `xml:"criterion"` 53 | TestRef string `xml:"test_ref,attr"` 54 | Comment string `xml:"comment,attr"` 55 | } 56 | 57 | // Affected : >definitions>definition>metadata>affected 58 | type Affected struct { 59 | XMLName xml.Name `xml:"affected"` 60 | Family string `xml:"family,attr"` 61 | Platforms []string `xml:"platform"` 62 | } 63 | 64 | // Reference : >definitions>definition>metadata>reference 65 | type Reference struct { 66 | XMLName xml.Name `xml:"reference"` 67 | Source string `xml:"source,attr"` 68 | RefID string `xml:"ref_id,attr"` 69 | RefURL string `xml:"ref_url,attr"` 70 | } 71 | 72 | // Advisory : >definitions>definition>metadata>advisory 73 | // RedHat and Ubuntu OVAL 74 | type Advisory struct { 75 | XMLName xml.Name `xml:"advisory"` 76 | Severity string `xml:"severity"` 77 | Cves []Cve `xml:"cve"` 78 | Bugzillas []Bugzilla `xml:"bugzilla"` 79 | AffectedCPEList []string `xml:"affected_cpe_list>cpe"` 80 | Issued struct { 81 | Date string `xml:"date,attr"` 82 | } `xml:"issued"` 83 | Updated struct { 84 | Date string `xml:"date,attr"` 85 | } `xml:"updated"` 86 | } 87 | 88 | // Cve : >definitions>definition>metadata>advisory>cve 89 | type Cve struct { 90 | XMLName xml.Name `xml:"cve"` 91 | CveID string `xml:",chardata"` 92 | Cvss3 string `xml:"cvss3,attr"` 93 | Impact string `xml:"impact,attr"` 94 | Href string `xml:"href,attr"` 95 | } 96 | 97 | // Bugzilla : >definitions>definition>metadata>advisory>bugzilla 98 | type Bugzilla struct { 99 | XMLName xml.Name `xml:"bugzilla"` 100 | URL string `xml:"href,attr"` 101 | Title string `xml:",chardata"` 102 | } 103 | 104 | // Tests : >tests 105 | type Tests struct { 106 | XMLName xml.Name `xml:"tests"` 107 | RpminfoTest []RpminfoTest `xml:"rpminfo_test"` 108 | } 109 | 110 | // RpminfoTest : >tests>rpminfo_test 111 | type RpminfoTest struct { 112 | ID string `xml:"id,attr"` 113 | Comment string `xml:"comment,attr"` 114 | Check string `xml:"check,attr"` 115 | Object ObjectRef `xml:"object"` 116 | State StateRef `xml:"state"` 117 | } 118 | 119 | // ObjectRef : >tests>rpminfo_test>object-object_ref 120 | type ObjectRef struct { 121 | XMLName xml.Name `xml:"object"` 122 | Text string `xml:",chardata"` 123 | ObjectRef string `xml:"object_ref,attr"` 124 | } 125 | 126 | // StateRef : >tests>rpminfo_test>state-state_ref 127 | type StateRef struct { 128 | XMLName xml.Name `xml:"state"` 129 | Text string `xml:",chardata"` 130 | StateRef string `xml:"state_ref,attr"` 131 | } 132 | 133 | // Objects : >objects 134 | type Objects struct { 135 | XMLName xml.Name `xml:"objects"` 136 | RpminfoObject []RpminfoObject `xml:"rpminfo_object"` 137 | } 138 | 139 | // RpminfoObject : >objects>rpminfo_object 140 | type RpminfoObject struct { 141 | ID string `xml:"id,attr"` 142 | Name string `xml:"name"` 143 | } 144 | 145 | // States : >states 146 | type States struct { 147 | XMLName xml.Name `xml:"states"` 148 | RpminfoState []RpminfoState `xml:"rpminfo_state"` 149 | } 150 | 151 | // RpminfoState : >states>rpminfo_state 152 | type RpminfoState struct { 153 | ID string `xml:"id,attr"` 154 | SignatureKeyid SignatureKeyid `xml:"signature_keyid"` 155 | Version struct { 156 | Text string `xml:",chardata"` 157 | Operation string `xml:"operation,attr"` 158 | } `xml:"version"` 159 | Arch struct { 160 | Text string `xml:",chardata"` 161 | Datatype string `xml:"datatype,attr"` 162 | Operation string `xml:"operation,attr"` 163 | } `xml:"arch"` 164 | Evr struct { 165 | Text string `xml:",chardata"` 166 | Datatype string `xml:"datatype,attr"` 167 | Operation string `xml:"operation,attr"` 168 | } `xml:"evr"` 169 | } 170 | 171 | // SignatureKeyid : >states>rpminfo_state>signature_keyid 172 | type SignatureKeyid struct { 173 | Text string `xml:",chardata"` 174 | Operation string `xml:"operation,attr"` 175 | } 176 | -------------------------------------------------------------------------------- /models/redhat/redhat.go: -------------------------------------------------------------------------------- 1 | package redhat 2 | 3 | import ( 4 | "fmt" 5 | "maps" 6 | "slices" 7 | "strings" 8 | "time" 9 | 10 | version "github.com/knqyf263/go-rpm-version" 11 | "github.com/spf13/viper" 12 | 13 | "github.com/vulsio/goval-dictionary/models" 14 | "github.com/vulsio/goval-dictionary/models/util" 15 | ) 16 | 17 | // ConvertToModel Convert OVAL to models 18 | func ConvertToModel(v string, roots []Root) []models.Definition { 19 | defs := map[string]models.Definition{} 20 | for _, root := range roots { 21 | for _, d := range root.Definitions.Definitions { 22 | if strings.HasPrefix(d.ID, "oval:com.redhat.unaffected:def:") || strings.Contains(d.Description, "** REJECT **") { 23 | continue 24 | } 25 | 26 | var cves = make([]models.Cve, 0, len(d.Advisory.Cves)) 27 | for _, c := range d.Advisory.Cves { 28 | cves = append(cves, models.Cve{ 29 | CveID: c.CveID, 30 | Cvss2: c.Cvss2, 31 | Cvss3: c.Cvss3, 32 | Cwe: c.Cwe, 33 | Impact: c.Impact, 34 | Href: c.Href, 35 | Public: c.Public, 36 | }) 37 | } 38 | 39 | var rs = make([]models.Reference, 0, len(d.References)) 40 | for _, r := range d.References { 41 | rs = append(rs, models.Reference{ 42 | Source: r.Source, 43 | RefID: r.RefID, 44 | RefURL: r.RefURL, 45 | }) 46 | } 47 | 48 | var cpes = make([]models.Cpe, 0, len(d.Advisory.AffectedCPEList)) 49 | for _, cpe := range d.Advisory.AffectedCPEList { 50 | cpes = append(cpes, models.Cpe{ 51 | Cpe: cpe, 52 | }) 53 | } 54 | 55 | var bs = make([]models.Bugzilla, 0, len(d.Advisory.Bugzillas)) 56 | for _, b := range d.Advisory.Bugzillas { 57 | bs = append(bs, models.Bugzilla{ 58 | BugzillaID: b.ID, 59 | URL: b.URL, 60 | Title: b.Title, 61 | }) 62 | } 63 | 64 | var ress = make([]models.Resolution, 0, len(d.Advisory.Affected.Resolution)) 65 | for _, r := range d.Advisory.Affected.Resolution { 66 | ress = append(ress, models.Resolution{ 67 | State: r.State, 68 | Components: func() []models.Component { 69 | var comps = make([]models.Component, 0, len(r.Component)) 70 | for _, c := range r.Component { 71 | comps = append(comps, models.Component{ 72 | Component: c, 73 | }) 74 | } 75 | return comps 76 | }(), 77 | }) 78 | } 79 | 80 | issued := util.ParsedOrDefaultTime([]string{"2006-01-02"}, d.Advisory.Issued.Date) 81 | updated := util.ParsedOrDefaultTime([]string{"2006-01-02"}, d.Advisory.Updated.Date) 82 | 83 | def := models.Definition{ 84 | DefinitionID: d.ID, 85 | Title: d.Title, 86 | Description: d.Description, 87 | Advisory: models.Advisory{ 88 | Severity: d.Advisory.Severity, 89 | Cves: cves, 90 | Bugzillas: bs, 91 | AffectedResolution: ress, 92 | AffectedCPEList: cpes, 93 | Issued: issued, 94 | Updated: updated, 95 | }, 96 | AffectedPacks: collectRedHatPacks(v, d.Criteria), 97 | References: rs, 98 | } 99 | 100 | if viper.GetBool("no-details") { 101 | def.Title = "" 102 | def.Description = "" 103 | def.Advisory.Severity = "" 104 | def.Advisory.AffectedCPEList = []models.Cpe{} 105 | def.Advisory.Bugzillas = []models.Bugzilla{} 106 | def.Advisory.Issued = time.Time{} 107 | def.Advisory.Updated = time.Time{} 108 | def.References = []models.Reference{} 109 | } 110 | 111 | if _, ok := defs[def.DefinitionID]; !ok { 112 | defs[def.DefinitionID] = def 113 | } 114 | } 115 | } 116 | return slices.Collect(maps.Values(defs)) 117 | } 118 | 119 | func collectRedHatPacks(v string, cri Criteria) []models.Package { 120 | pkgs := map[string]models.Package{} 121 | for _, p := range walkRedHat(cri, []models.Package{}, "") { 122 | n := p.Name 123 | if p.ModularityLabel != "" { 124 | n = fmt.Sprintf("%s::%s", p.ModularityLabel, p.Name) 125 | } 126 | 127 | if p.NotFixedYet { 128 | pkgs[n] = p 129 | continue 130 | } 131 | 132 | // OVALv1 includes definitions other than the target RHEL version 133 | if !strings.Contains(p.Version, ".el"+v) && !strings.Contains(p.Version, ".module+el"+v) { 134 | continue 135 | } 136 | 137 | // since different versions are defined for the same package, the newer version is adopted 138 | // example: OVALv2: oval:com.redhat.rhsa:def:20111349, oval:com.redhat.rhsa:def:20120451 139 | if base, ok := pkgs[n]; ok { 140 | v1 := version.NewVersion(base.Version) 141 | v2 := version.NewVersion(p.Version) 142 | if v1.GreaterThan(v2) { 143 | p = base 144 | } 145 | } 146 | 147 | pkgs[n] = p 148 | } 149 | return slices.Collect(maps.Values(pkgs)) 150 | } 151 | 152 | func walkRedHat(cri Criteria, acc []models.Package, label string) []models.Package { 153 | for _, c := range cri.Criterions { 154 | switch { 155 | case strings.HasPrefix(c.Comment, "Module ") && strings.HasSuffix(c.Comment, " is enabled"): 156 | label = strings.TrimSuffix(strings.TrimPrefix(c.Comment, "Module "), " is enabled") 157 | case strings.Contains(c.Comment, " is earlier than "): 158 | ss := strings.Split(c.Comment, " is earlier than ") 159 | if len(ss) != 2 { 160 | continue 161 | } 162 | acc = append(acc, models.Package{ 163 | Name: ss[0], 164 | Version: strings.Split(ss[1], " ")[0], 165 | ModularityLabel: label, 166 | }) 167 | case !strings.HasPrefix(c.Comment, "Red Hat Enterprise Linux") && !strings.HasPrefix(c.Comment, "Red Hat CoreOS") && strings.HasSuffix(c.Comment, " is installed"): 168 | acc = append(acc, models.Package{ 169 | Name: strings.TrimSuffix(c.Comment, " is installed"), 170 | ModularityLabel: label, 171 | NotFixedYet: true, 172 | }) 173 | } 174 | } 175 | 176 | if len(cri.Criterias) == 0 { 177 | return acc 178 | } 179 | for _, c := range cri.Criterias { 180 | acc = walkRedHat(c, acc, label) 181 | } 182 | return acc 183 | } 184 | -------------------------------------------------------------------------------- /commands/select.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/k0kubun/pp" 8 | "github.com/spf13/cobra" 9 | "github.com/spf13/viper" 10 | "golang.org/x/xerrors" 11 | 12 | "github.com/vulsio/goval-dictionary/config" 13 | "github.com/vulsio/goval-dictionary/db" 14 | "github.com/vulsio/goval-dictionary/models" 15 | ) 16 | 17 | // SelectCmd is Subcommand for fetch RedHat OVAL 18 | var selectCmd = &cobra.Command{ 19 | Use: "select", 20 | Short: "Select from DB", 21 | Long: `Select from DB`, 22 | } 23 | 24 | func init() { 25 | RootCmd.AddCommand(selectCmd) 26 | 27 | selectCmd.AddCommand( 28 | &cobra.Command{ 29 | Use: "package ()", 30 | Short: "Select OVAL by package name", 31 | Args: cobra.RangeArgs(3, 4), 32 | RunE: func(_ *cobra.Command, args []string) error { 33 | arch := "" 34 | if len(args) == 4 { 35 | switch args[0] { 36 | case config.Amazon, config.Oracle, config.Fedora: 37 | arch = args[3] 38 | default: 39 | return xerrors.Errorf("Family: %s cannot use the Architecture argument.", args[0]) 40 | } 41 | } 42 | 43 | driver, err := db.NewDB(viper.GetString("dbtype"), viper.GetString("dbpath"), viper.GetBool("debug-sql"), db.Option{}) 44 | if err != nil { 45 | if errors.Is(err, db.ErrDBLocked) { 46 | return xerrors.Errorf("Failed to open DB. Close DB connection before fetching. err: %w", err) 47 | } 48 | return xerrors.Errorf("Failed to open DB. err: %w", err) 49 | } 50 | 51 | fetchMeta, err := driver.GetFetchMeta() 52 | if err != nil { 53 | return xerrors.Errorf("Failed to get FetchMeta from DB. err: %w", err) 54 | } 55 | if fetchMeta.OutDated() { 56 | return xerrors.Errorf("Failed to select command. err: SchemaVersion is old. SchemaVersion: %+v", map[string]uint{"latest": models.LatestSchemaVersion, "DB": fetchMeta.SchemaVersion}) 57 | } 58 | 59 | dfs, err := driver.GetByPackName(args[0], args[1], args[2], arch) 60 | if err != nil { 61 | return xerrors.Errorf("Failed to get cve by package. err: %w", err) 62 | } 63 | 64 | for _, d := range dfs { 65 | for _, cve := range d.Advisory.Cves { 66 | fmt.Printf("%s\n", cve.CveID) 67 | for _, pack := range d.AffectedPacks { 68 | fmt.Printf(" %v\n", pack) 69 | } 70 | } 71 | } 72 | fmt.Println("------------------") 73 | pp.ColoringEnabled = false 74 | _, _ = pp.Println(dfs) 75 | 76 | return nil 77 | }, 78 | Example: `$ goval-dictionary select package ubuntu 24.04 bash 79 | $ goval-dictionary select package oracle 9 bash x86_64`, 80 | }, 81 | &cobra.Command{ 82 | Use: "cve-id ()", 83 | Short: "Select OVAL by CVE-ID", 84 | Args: cobra.RangeArgs(3, 4), 85 | RunE: func(_ *cobra.Command, args []string) error { 86 | arch := "" 87 | if len(args) == 4 { 88 | switch args[0] { 89 | case config.Amazon, config.Oracle, config.Fedora: 90 | arch = args[3] 91 | default: 92 | return xerrors.Errorf("Family: %s cannot use the Architecture argument.", args[0]) 93 | } 94 | } 95 | 96 | driver, err := db.NewDB(viper.GetString("dbtype"), viper.GetString("dbpath"), viper.GetBool("debug-sql"), db.Option{}) 97 | if err != nil { 98 | if errors.Is(err, db.ErrDBLocked) { 99 | return xerrors.Errorf("Failed to open DB. Close DB connection before fetching. err: %w", err) 100 | } 101 | return xerrors.Errorf("Failed to open DB. err: %w", err) 102 | } 103 | 104 | fetchMeta, err := driver.GetFetchMeta() 105 | if err != nil { 106 | return xerrors.Errorf("Failed to get FetchMeta from DB. err: %w", err) 107 | } 108 | if fetchMeta.OutDated() { 109 | return xerrors.Errorf("Failed to select command. err: SchemaVersion is old. SchemaVersion: %+v", map[string]uint{"latest": models.LatestSchemaVersion, "DB": fetchMeta.SchemaVersion}) 110 | } 111 | 112 | dfs, err := driver.GetByCveID(args[0], args[1], args[2], arch) 113 | if err != nil { 114 | return xerrors.Errorf("Failed to get cve by cveID. err: %w", err) 115 | } 116 | for _, d := range dfs { 117 | fmt.Printf("%s\n", d.Title) 118 | fmt.Printf("%v\n", d.Advisory.Cves) 119 | } 120 | fmt.Println("------------------") 121 | pp.ColoringEnabled = false 122 | _, _ = pp.Println(dfs) 123 | 124 | return nil 125 | }, 126 | Example: `$ goval-dictionary select cve-id ubuntu 24.04 CVE-2024-6387 127 | $ goval-dictionary select cve-id oracle 9 CVE-2024-6387 x86_64`, 128 | }, 129 | &cobra.Command{ 130 | Use: "advisories ", 131 | Short: "List Advisories and Releated CVE-IDs", 132 | Args: cobra.ExactArgs(2), 133 | RunE: func(_ *cobra.Command, args []string) error { 134 | driver, err := db.NewDB(viper.GetString("dbtype"), viper.GetString("dbpath"), viper.GetBool("debug-sql"), db.Option{}) 135 | if err != nil { 136 | if errors.Is(err, db.ErrDBLocked) { 137 | return xerrors.Errorf("Failed to open DB. Close DB connection before fetching. err: %w", err) 138 | } 139 | return xerrors.Errorf("Failed to open DB. err: %w", err) 140 | } 141 | 142 | fetchMeta, err := driver.GetFetchMeta() 143 | if err != nil { 144 | return xerrors.Errorf("Failed to get FetchMeta from DB. err: %w", err) 145 | } 146 | if fetchMeta.OutDated() { 147 | return xerrors.Errorf("Failed to select command. err: SchemaVersion is old. SchemaVersion: %+v", map[string]uint{"latest": models.LatestSchemaVersion, "DB": fetchMeta.SchemaVersion}) 148 | } 149 | 150 | m, err := driver.GetAdvisories(args[0], args[1]) 151 | if err != nil { 152 | return xerrors.Errorf("Failed to get cve by cveID. err: %w", err) 153 | } 154 | pp.ColoringEnabled = false 155 | _, _ = pp.Println(m) 156 | 157 | return nil 158 | }, 159 | Example: `$ goval-dictionary select advisories ubuntu 24.04`, 160 | }, 161 | ) 162 | 163 | } 164 | -------------------------------------------------------------------------------- /fetcher/util/fetcher.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "bytes" 5 | "compress/bzip2" 6 | "compress/gzip" 7 | "fmt" 8 | "io" 9 | "net/http" 10 | "net/url" 11 | "sync" 12 | "time" 13 | 14 | "github.com/inconshreveable/log15" 15 | "github.com/klauspost/compress/zstd" 16 | "github.com/spf13/viper" 17 | "github.com/ulikunitz/xz" 18 | "golang.org/x/xerrors" 19 | 20 | "github.com/vulsio/goval-dictionary/config" 21 | ) 22 | 23 | // MIMEType : 24 | type MIMEType int 25 | 26 | const ( 27 | // MIMETypeUnknown : 28 | MIMETypeUnknown MIMEType = iota 29 | // MIMETypeXML : 30 | MIMETypeXML 31 | // MIMETypeTxt : 32 | MIMETypeTxt 33 | // MIMETypeJSON : 34 | MIMETypeJSON 35 | // MIMETypeYml : 36 | MIMETypeYml 37 | // MIMETypeHTML : 38 | MIMETypeHTML 39 | // MIMETypeBzip2 : 40 | MIMETypeBzip2 41 | // MIMETypeXz : 42 | MIMETypeXz 43 | // MIMETypeGzip : 44 | MIMETypeGzip 45 | // MIMETypeZst : 46 | MIMETypeZst 47 | ) 48 | 49 | func (m MIMEType) String() string { 50 | switch m { 51 | case MIMETypeXML: 52 | return "xml" 53 | case MIMETypeTxt: 54 | return "txt" 55 | case MIMETypeJSON: 56 | return "json" 57 | case MIMETypeYml: 58 | return "yml" 59 | case MIMETypeHTML: 60 | return "html" 61 | case MIMETypeBzip2: 62 | return "bzip2" 63 | case MIMETypeXz: 64 | return "xz" 65 | case MIMETypeGzip: 66 | return "gz" 67 | case MIMETypeZst: 68 | return "zst" 69 | default: 70 | return "Unknown" 71 | } 72 | } 73 | 74 | // FetchRequest has url, mimetype and fetch option 75 | type FetchRequest struct { 76 | Target string 77 | URL string 78 | MIMEType MIMEType 79 | LogSuppressed bool 80 | } 81 | 82 | // FetchResult has url and OVAL definitions 83 | type FetchResult struct { 84 | Target string 85 | URL string 86 | Body []byte 87 | LogSuppressed bool 88 | } 89 | 90 | // genWorkers generate workers 91 | func genWorkers(num int) chan<- func() { 92 | tasks := make(chan func()) 93 | for i := 0; i < num; i++ { 94 | go func() { 95 | for f := range tasks { 96 | f() 97 | } 98 | }() 99 | } 100 | return tasks 101 | } 102 | 103 | // FetchFeedFiles : 104 | func FetchFeedFiles(reqs []FetchRequest) (results []FetchResult, err error) { 105 | reqChan := make(chan FetchRequest, len(reqs)) 106 | resChan := make(chan FetchResult, len(reqs)) 107 | errChan := make(chan error, len(reqs)) 108 | defer close(reqChan) 109 | defer close(resChan) 110 | defer close(errChan) 111 | 112 | for _, r := range reqs { 113 | if !r.LogSuppressed { 114 | log15.Info("Fetching... ", "URL", r.URL) 115 | } 116 | } 117 | 118 | go func() { 119 | for _, r := range reqs { 120 | reqChan <- r 121 | } 122 | }() 123 | 124 | concurrency := len(reqs) 125 | tasks := genWorkers(concurrency) 126 | wg := new(sync.WaitGroup) 127 | for range reqs { 128 | wg.Add(1) 129 | tasks <- func() { 130 | req := <-reqChan 131 | body, err := fetchFileWithUA(req) 132 | wg.Done() 133 | if err != nil { 134 | errChan <- err 135 | return 136 | } 137 | resChan <- FetchResult{ 138 | Target: req.Target, 139 | URL: req.URL, 140 | Body: body, 141 | LogSuppressed: req.LogSuppressed, 142 | } 143 | } 144 | } 145 | wg.Wait() 146 | 147 | errs := []error{} 148 | timeout := time.After(10 * 60 * time.Second) 149 | for range reqs { 150 | select { 151 | case res := <-resChan: 152 | results = append(results, res) 153 | case err := <-errChan: 154 | errs = append(errs, err) 155 | case <-timeout: 156 | return results, fmt.Errorf("Timeout Fetching") 157 | } 158 | } 159 | if 0 < len(errs) { 160 | return results, fmt.Errorf("%s", errs) 161 | } 162 | return results, nil 163 | } 164 | 165 | func fetchFileWithUA(req FetchRequest) (body []byte, err error) { 166 | var proxyURL *url.URL 167 | var resp *http.Response 168 | 169 | httpClient := &http.Client{} 170 | httpProxy := viper.GetString("http-proxy") 171 | if httpProxy != "" { 172 | if proxyURL, err = url.Parse(httpProxy); err != nil { 173 | return nil, xerrors.Errorf("Failed to parse proxy url. err: %w", err) 174 | } 175 | httpClient = &http.Client{Transport: &http.Transport{Proxy: http.ProxyURL(proxyURL)}} 176 | } 177 | 178 | httpreq, err := http.NewRequest("GET", req.URL, nil) 179 | if err != nil { 180 | return nil, xerrors.Errorf("Failed to download. err: %w", err) 181 | } 182 | 183 | httpreq.Header.Set("User-Agent", fmt.Sprintf("goval-dictionary/%s.%s", config.Version, config.Revision)) 184 | resp, err = httpClient.Do(httpreq) 185 | if err != nil { 186 | return nil, xerrors.Errorf("Failed to download. err: %w", err) 187 | } 188 | defer resp.Body.Close() 189 | 190 | if resp.StatusCode != 200 { 191 | return nil, fmt.Errorf("Failed to HTTP GET. url: %s, response: %+v", req.URL, resp) 192 | } 193 | 194 | buf := bytes.Buffer{} 195 | if _, err := io.Copy(&buf, resp.Body); err != nil { 196 | return nil, err 197 | } 198 | 199 | var b bytes.Buffer 200 | switch req.MIMEType { 201 | case MIMETypeXML, MIMETypeTxt, MIMETypeJSON, MIMETypeYml, MIMETypeHTML: 202 | b = buf 203 | case MIMETypeBzip2: 204 | if _, err := b.ReadFrom(bzip2.NewReader(bytes.NewReader(buf.Bytes()))); err != nil { 205 | return nil, xerrors.Errorf("Failed to open bzip2 file. err: %w", err) 206 | } 207 | case MIMETypeXz: 208 | r, err := xz.NewReader(bytes.NewReader(buf.Bytes())) 209 | if err != nil { 210 | return nil, xerrors.Errorf("Failed to open xz file. err: %w", err) 211 | } 212 | if _, err = b.ReadFrom(r); err != nil { 213 | return nil, xerrors.Errorf("Failed to read xz file. err: %w", err) 214 | } 215 | case MIMETypeGzip: 216 | r, err := gzip.NewReader(bytes.NewReader(buf.Bytes())) 217 | if err != nil { 218 | return nil, xerrors.Errorf("Failed to open gzip file. err: %w", err) 219 | } 220 | if _, err = b.ReadFrom(r); err != nil { 221 | return nil, xerrors.Errorf("Failed to read gzip file. err: %w", err) 222 | } 223 | case MIMETypeZst: 224 | r, err := zstd.NewReader(bytes.NewReader(buf.Bytes())) 225 | if err != nil { 226 | return nil, xerrors.Errorf("Failed to open zstd file. err: %w", err) 227 | } 228 | if _, err = b.ReadFrom(r); err != nil { 229 | return nil, xerrors.Errorf("Failed to read zstd file. err: %w", err) 230 | } 231 | default: 232 | return nil, xerrors.Errorf("unexpected request MIME Type. expected: %q, actual: %q", []MIMEType{MIMETypeXML, MIMETypeTxt, MIMETypeJSON, MIMETypeYml, MIMETypeHTML, MIMETypeBzip2, MIMETypeXz, MIMETypeGzip, MIMETypeZst}, req.MIMEType) 233 | } 234 | 235 | return b.Bytes(), nil 236 | } 237 | -------------------------------------------------------------------------------- /models/ubuntu/ubuntu_test.go: -------------------------------------------------------------------------------- 1 | package ubuntu 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/k0kubun/pp" 8 | 9 | "github.com/vulsio/goval-dictionary/models" 10 | ) 11 | 12 | func TestCollectUbuntuPacks(t *testing.T) { 13 | var tests = []struct { 14 | cri Criteria 15 | tests map[string]dpkgInfoTest 16 | expected []models.Package 17 | }{ 18 | { 19 | cri: Criteria{ 20 | Criterions: []Criterion{ 21 | { 22 | TestRef: "oval:com.ubuntu.jammy:tst:200224390000040", 23 | Comment: "gcc-snapshot package in jammy, is related to the CVE in some way and has been fixed (note: '20140405-0ubuntu1').", 24 | }, 25 | }, 26 | }, 27 | tests: map[string]dpkgInfoTest{ 28 | "oval:com.ubuntu.jammy:tst:200224390000040": { 29 | Name: "gcc-snapshot", 30 | FixedVersion: "0:20140405-0ubuntu1", 31 | }, 32 | }, 33 | expected: []models.Package{}, 34 | }, 35 | { 36 | cri: Criteria{ 37 | Criterions: []Criterion{ 38 | { 39 | TestRef: "oval:com.ubuntu.jammy:tst:2018128860000050", 40 | Comment: "gcc-snapshot: while related to the CVE in some way, a decision has been made to ignore this issue.", 41 | }, 42 | }, 43 | }, 44 | tests: map[string]dpkgInfoTest{ 45 | "oval:com.ubuntu.jammy:tst:2018128860000050": {Name: "gcc-snapshot"}, 46 | }, 47 | expected: []models.Package{ 48 | { 49 | Name: "gcc-snapshot", 50 | NotFixedYet: true, 51 | }, 52 | }, 53 | }, 54 | { 55 | cri: Criteria{ 56 | Criterions: []Criterion{ 57 | { 58 | TestRef: "oval:com.ubuntu.jammy:tst:200224390000000", 59 | Comment: "gcc-arm-none-eabi package in jammy is affected and may need fixing.", 60 | }, 61 | }, 62 | }, 63 | tests: map[string]dpkgInfoTest{ 64 | "oval:com.ubuntu.jammy:tst:200224390000000": {Name: "gcc-arm-none-eabi"}, 65 | }, 66 | expected: []models.Package{}, 67 | }, 68 | { 69 | cri: Criteria{ 70 | Criterions: []Criterion{ 71 | { 72 | TestRef: "oval:com.ubuntu.jammy:tst:200224390000000", 73 | Comment: "gcc-arm-none-eabi package in jammy is affected and needs fixing.", 74 | }, 75 | }, 76 | }, 77 | tests: map[string]dpkgInfoTest{ 78 | "oval:com.ubuntu.jammy:tst:200224390000000": {Name: "gcc-arm-none-eabi"}, 79 | }, 80 | expected: []models.Package{ 81 | { 82 | Name: "gcc-arm-none-eabi", 83 | NotFixedYet: true, 84 | }, 85 | }, 86 | }, 87 | { 88 | cri: Criteria{ 89 | Criterions: []Criterion{ 90 | { 91 | TestRef: "oval:com.ubuntu.jammy:tst:2018128860000050", 92 | Comment: "gcc-snapshot package in jammy is affected, but a decision has been made to defer addressing it.", 93 | }, 94 | }, 95 | }, 96 | tests: map[string]dpkgInfoTest{ 97 | "oval:com.ubuntu.jammy:tst:2018128860000050": {Name: "gcc-snapshot"}, 98 | }, 99 | expected: []models.Package{ 100 | { 101 | Name: "gcc-snapshot", 102 | NotFixedYet: true, 103 | }, 104 | }, 105 | }, 106 | { 107 | cri: Criteria{ 108 | Criterions: []Criterion{ 109 | { 110 | TestRef: "oval:com.ubuntu.xenial:tst:2020143720000010", 111 | Comment: "grub2-unsigned package in xenial is affected. An update containing the fix has been completed and is pending publication (note: '2.04-1ubuntu42').", 112 | }, 113 | }, 114 | }, 115 | tests: map[string]dpkgInfoTest{ 116 | "oval:com.ubuntu.xenial:tst:2020143720000010": { 117 | Name: "grub2-unsigned", 118 | FixedVersion: "0:2.04-1ubuntu42", 119 | }, 120 | }, 121 | expected: []models.Package{ 122 | { 123 | Name: "grub2-unsigned", 124 | NotFixedYet: true, 125 | }, 126 | }, 127 | }, 128 | { 129 | cri: Criteria{ 130 | Criterions: []Criterion{ 131 | { 132 | TestRef: "oval:com.ubuntu.jammy:tst:2021285440000000", 133 | Comment: "subversion package in jammy was vulnerable but has been fixed (note: '1.14.1-3ubuntu0.22.04.1').", 134 | }, 135 | }, 136 | }, 137 | tests: map[string]dpkgInfoTest{ 138 | "oval:com.ubuntu.jammy:tst:2021285440000000": { 139 | Name: "subversion", 140 | FixedVersion: "0:1.14.1-3ubuntu0.22.04.1", 141 | }, 142 | }, 143 | expected: []models.Package{ 144 | { 145 | Name: "subversion", 146 | Version: "0:1.14.1-3ubuntu0.22.04.1", 147 | NotFixedYet: false, 148 | }, 149 | }, 150 | }, 151 | { 152 | cri: Criteria{ 153 | Criterions: []Criterion{ 154 | { 155 | TestRef: "oval:com.ubuntu.jammy:tst:2021299550000000", 156 | Comment: "firefox package in jammy was vulnerable and has been fixed, but no release version available for it.", 157 | }, 158 | }, 159 | }, 160 | tests: map[string]dpkgInfoTest{ 161 | "oval:com.ubuntu.jammy:tst:2021299550000000": {Name: "firefox"}, 162 | }, 163 | expected: []models.Package{ 164 | { 165 | Name: "firefox", 166 | Version: "", 167 | NotFixedYet: false, 168 | }, 169 | }, 170 | }, 171 | { 172 | cri: Criteria{ 173 | Criterions: []Criterion{ 174 | { 175 | TestRef: "oval:com.ubuntu.jammy:tst:2016107230000000", 176 | Comment: "Is kernel linux running", 177 | }, 178 | { 179 | TestRef: "oval:com.ubuntu.jammy:tst:2018121260000030", 180 | Comment: "kernel version comparison", 181 | }, 182 | }, 183 | }, 184 | tests: map[string]dpkgInfoTest{}, 185 | expected: []models.Package{}, 186 | }, 187 | { 188 | cri: Criteria{ 189 | Criterias: []Criteria{ 190 | { 191 | Criterions: []Criterion{ 192 | { 193 | TestRef: "oval:com.ubuntu.jammy:tst:200901660000010", 194 | Comment: "poppler package in jammy was vulnerable but has been fixed (note: '0.10.5-1ubuntu2').", 195 | }, 196 | }, 197 | }, 198 | }, 199 | }, 200 | tests: map[string]dpkgInfoTest{ 201 | "oval:com.ubuntu.jammy:tst:200901660000010": { 202 | Name: "poppler", 203 | FixedVersion: "0:0.10.5-1ubuntu2", 204 | }, 205 | }, 206 | expected: []models.Package{ 207 | { 208 | Name: "poppler", 209 | Version: "0:0.10.5-1ubuntu2", 210 | NotFixedYet: false, 211 | }, 212 | }, 213 | }, 214 | } 215 | 216 | for i, tt := range tests { 217 | if actual := collectUbuntuPacks(tt.cri, tt.tests); !reflect.DeepEqual(tt.expected, actual) { 218 | e := pp.Sprintf("%v", tt.expected) 219 | a := pp.Sprintf("%v", actual) 220 | t.Errorf("[%d]: expected: %s\n, actual: %s\n", i, e, a) 221 | } 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /models/ubuntu/types.go: -------------------------------------------------------------------------------- 1 | package ubuntu 2 | 3 | import "encoding/xml" 4 | 5 | // Root : root object 6 | type Root struct { 7 | XMLName xml.Name `xml:"oval_definitions"` 8 | Generator Generator `xml:"generator"` 9 | Definitions Definitions `xml:"definitions"` 10 | Tests Tests `xml:"tests"` 11 | Objects Objects `xml:"objects"` 12 | States States `xml:"states"` 13 | Variables Variables `xml:"variables"` 14 | } 15 | 16 | // Generator : >generator 17 | type Generator struct { 18 | XMLName xml.Name `xml:"generator"` 19 | ProductName string `xml:"product_name"` 20 | ProductVersion string `xml:"product_version"` 21 | SchemaVersion string `xml:"schema_version"` 22 | Timestamp string `xml:"timestamp"` 23 | } 24 | 25 | // Definitions : >definitions 26 | type Definitions struct { 27 | XMLName xml.Name `xml:"definitions"` 28 | Definitions []Definition `xml:"definition"` 29 | } 30 | 31 | // Definition : >definitions>definition 32 | type Definition struct { 33 | XMLName xml.Name `xml:"definition"` 34 | ID string `xml:"id,attr"` 35 | Class string `xml:"class,attr"` 36 | Title string `xml:"metadata>title"` 37 | Affecteds []Affected `xml:"metadata>affected"` 38 | References []Reference `xml:"metadata>reference"` 39 | Description string `xml:"metadata>description"` 40 | Advisory Advisory `xml:"metadata>advisory"` 41 | Notes struct { 42 | Text string `xml:",chardata"` 43 | Note string `xml:"note"` 44 | } `xml:"notes"` 45 | Criteria Criteria `xml:"criteria"` 46 | } 47 | 48 | // Criteria : >definitions>definition>criteria 49 | type Criteria struct { 50 | XMLName xml.Name `xml:"criteria"` 51 | Operator string `xml:"operator,attr"` 52 | Criterias []Criteria `xml:"criteria"` 53 | Criterions []Criterion `xml:"criterion"` 54 | } 55 | 56 | // Criterion : >definitions>definition>criteria>*>criterion 57 | type Criterion struct { 58 | XMLName xml.Name `xml:"criterion"` 59 | TestRef string `xml:"test_ref,attr"` 60 | Comment string `xml:"comment,attr"` 61 | } 62 | 63 | // Affected : >definitions>definition>metadata>affected 64 | type Affected struct { 65 | XMLName xml.Name `xml:"affected"` 66 | Family string `xml:"family,attr"` 67 | Platforms []string `xml:"platform"` 68 | } 69 | 70 | // Reference : >definitions>definition>metadata>reference 71 | type Reference struct { 72 | XMLName xml.Name `xml:"reference"` 73 | Source string `xml:"source,attr"` 74 | RefID string `xml:"ref_id,attr"` 75 | RefURL string `xml:"ref_url,attr"` 76 | } 77 | 78 | // Advisory : >definitions>definition>metadata>advisory 79 | type Advisory struct { 80 | XMLName xml.Name `xml:"advisory"` 81 | Severity string `xml:"severity"` 82 | Rights string `xml:"rights"` 83 | PublicDate string `xml:"public_date"` 84 | Refs []Ref `xml:"ref"` 85 | Bugs []Bug `xml:"bug"` 86 | } 87 | 88 | // Ref : >definitions>definition>metadata>advisory>ref 89 | type Ref struct { 90 | XMLName xml.Name `xml:"ref"` 91 | URL string `xml:",chardata"` 92 | } 93 | 94 | // Bug : >definitions>definition>metadata>advisory>bug 95 | type Bug struct { 96 | XMLName xml.Name `xml:"bug"` 97 | URL string `xml:",chardata"` 98 | } 99 | 100 | // Tests : >tests 101 | type Tests struct { 102 | XMLName xml.Name `xml:"tests"` 103 | Textfilecontent54Test []Textfilecontent54Test `xml:"textfilecontent54_test"` 104 | } 105 | 106 | // Textfilecontent54Test : >tests>textfilecontent54_test 107 | type Textfilecontent54Test struct { 108 | ID string `xml:"id,attr"` 109 | Check string `xml:"check,attr"` 110 | CheckExistence string `xml:"check_existence,attr"` 111 | Comment string `xml:"comment,attr"` 112 | Object ObjectRef `xml:"object"` 113 | State StateRef `xml:"state"` 114 | } 115 | 116 | // ObjectRef : >tests>textfilecontent54_test>object-object_ref 117 | type ObjectRef struct { 118 | XMLName xml.Name `xml:"object"` 119 | Text string `xml:",chardata"` 120 | ObjectRef string `xml:"object_ref,attr"` 121 | } 122 | 123 | // StateRef : >tests>textfilecontent54_test>state-state_ref 124 | type StateRef struct { 125 | XMLName xml.Name `xml:"state"` 126 | Text string `xml:",chardata"` 127 | StateRef string `xml:"state_ref,attr"` 128 | } 129 | 130 | // Objects : >objects 131 | type Objects struct { 132 | XMLName xml.Name `xml:"objects"` 133 | Textfilecontent54Object []Textfilecontent54Object `xml:"textfilecontent54_object"` 134 | } 135 | 136 | // Textfilecontent54Object : >objects>textfilecontent54_object 137 | type Textfilecontent54Object struct { 138 | ID string `xml:"id,attr"` 139 | Comment string `xml:"comment,attr"` 140 | Path string `xml:"path"` 141 | Filename string `xml:"filename"` 142 | Pattern struct { 143 | Text string `xml:",chardata"` 144 | Operation string `xml:"operation,attr"` 145 | Datatype string `xml:"datatype,attr"` 146 | VarRef string `xml:"var_ref,attr"` 147 | VarCheck string `xml:"var_check,attr"` 148 | } `xml:"pattern"` 149 | Instance struct { 150 | Text string `xml:",chardata"` 151 | Operation string `xml:"operation,attr"` 152 | Datatype string `xml:"datatype,attr"` 153 | } `xml:"instance"` 154 | } 155 | 156 | // States : >states 157 | type States struct { 158 | XMLName xml.Name `xml:"states"` 159 | Textfilecontent54State []Textfilecontent54State `xml:"textfilecontent54_state"` 160 | } 161 | 162 | // Textfilecontent54State : >states>textfilecontent54_state 163 | type Textfilecontent54State struct { 164 | ID string `xml:"id,attr"` 165 | Comment string `xml:"comment,attr"` 166 | Subexpression struct { 167 | Text string `xml:",chardata"` 168 | Datatype string `xml:"datatype,attr"` 169 | Operation string `xml:"operation,attr"` 170 | } `xml:"subexpression"` 171 | } 172 | 173 | // Variables : >variables 174 | type Variables struct { 175 | XMLName xml.Name `xml:"variables"` 176 | ConstantVariable []ConstantVariable `xml:"constant_variable"` 177 | } 178 | 179 | // ConstantVariable : >variables>constant_variable 180 | type ConstantVariable struct { 181 | Text string `xml:",chardata"` 182 | ID string `xml:"id,attr"` 183 | Version string `xml:"version,attr"` 184 | Datatype string `xml:"datatype,attr"` 185 | Comment string `xml:"comment,attr"` 186 | Value []string `xml:"value"` 187 | } 188 | 189 | type dpkgInfoTest struct { 190 | Name string 191 | FixedVersion string 192 | } 193 | -------------------------------------------------------------------------------- /models/debian/types.go: -------------------------------------------------------------------------------- 1 | package debian 2 | 3 | import "encoding/xml" 4 | 5 | // Root : root object 6 | type Root struct { 7 | XMLName xml.Name `xml:"oval_definitions"` 8 | Generator Generator `xml:"generator"` 9 | Definitions Definitions `xml:"definitions"` 10 | Tests Tests `xml:"tests"` 11 | Objects Objects `xml:"objects"` 12 | States States `xml:"states"` 13 | } 14 | 15 | // Generator : >generator 16 | type Generator struct { 17 | XMLName xml.Name `xml:"generator"` 18 | ProductName string `xml:"product_name"` 19 | SchemaVersion string `xml:"schema_version"` 20 | Timestamp string `xml:"timestamp"` 21 | } 22 | 23 | // Definitions : >definitions 24 | type Definitions struct { 25 | XMLName xml.Name `xml:"definitions"` 26 | Definitions []Definition `xml:"definition"` 27 | } 28 | 29 | // Definition : >definitions>definition 30 | type Definition struct { 31 | XMLName xml.Name `xml:"definition"` 32 | ID string `xml:"id,attr"` 33 | Class string `xml:"class,attr"` 34 | Title string `xml:"metadata>title"` 35 | Affecteds []Affected `xml:"metadata>affected"` 36 | References []Reference `xml:"metadata>reference"` 37 | Description string `xml:"metadata>description"` 38 | Debian Debian `xml:"metadata>debian"` 39 | Criteria Criteria `xml:"criteria"` 40 | } 41 | 42 | // Criteria : >definitions>definition>criteria 43 | type Criteria struct { 44 | XMLName xml.Name `xml:"criteria"` 45 | Operator string `xml:"operator,attr"` 46 | Criterias []Criteria `xml:"criteria"` 47 | Criterions []Criterion `xml:"criterion"` 48 | } 49 | 50 | // Criterion : >definitions>definition>criteria>*>criterion 51 | type Criterion struct { 52 | XMLName xml.Name `xml:"criterion"` 53 | TestRef string `xml:"test_ref,attr"` 54 | Comment string `xml:"comment,attr"` 55 | } 56 | 57 | // Affected : >definitions>definition>metadata>affected 58 | type Affected struct { 59 | XMLName xml.Name `xml:"affected"` 60 | Family string `xml:"family,attr"` 61 | Platforms []string `xml:"platform"` 62 | Products []string `xml:"product"` 63 | } 64 | 65 | // Reference : >definitions>definition>metadata>reference 66 | type Reference struct { 67 | XMLName xml.Name `xml:"reference"` 68 | Source string `xml:"source,attr"` 69 | RefID string `xml:"ref_id,attr"` 70 | RefURL string `xml:"ref_url,attr"` 71 | } 72 | 73 | // Debian : >definitions>definition>metadata>debian 74 | type Debian struct { 75 | XMLName xml.Name `xml:"debian"` 76 | DSA string `xml:"dsa"` 77 | MoreInfo string `xml:"moreinfo"` 78 | Date string `xml:"date"` 79 | } 80 | 81 | // Tests : >tests 82 | type Tests struct { 83 | XMLName xml.Name `xml:"tests"` 84 | Textfilecontent54Test Textfilecontent54Test `xml:"textfilecontent54_test"` 85 | UnameTest UnameTest `xml:"uname_test"` 86 | DpkginfoTest []DpkginfoTest `xml:"dpkginfo_test"` 87 | } 88 | 89 | // Textfilecontent54Test : >tests>textfilecontent54_test 90 | type Textfilecontent54Test struct { 91 | Text string `xml:",chardata"` 92 | Check string `xml:"check,attr"` 93 | CheckExistence string `xml:"check_existence,attr"` 94 | Comment string `xml:"comment,attr"` 95 | ID string `xml:"id,attr"` 96 | Object ObjectRef `xml:"object"` 97 | State StateRef `xml:"state"` 98 | } 99 | 100 | // UnameTest : >tests>uname_test 101 | type UnameTest struct { 102 | Text string `xml:",chardata"` 103 | Check string `xml:"check,attr"` 104 | CheckExistence string `xml:"check_existence,attr"` 105 | Comment string `xml:"comment,attr"` 106 | ID string `xml:"id,attr"` 107 | Object ObjectRef `xml:"object"` 108 | } 109 | 110 | // DpkginfoTest : >tests>dpkginfo_test 111 | type DpkginfoTest struct { 112 | Text string `xml:",chardata"` 113 | Check string `xml:"check,attr"` 114 | CheckExistence string `xml:"check_existence,attr"` 115 | Comment string `xml:"comment,attr"` 116 | ID string `xml:"id,attr"` 117 | Object ObjectRef `xml:"object"` 118 | State StateRef `xml:"state"` 119 | } 120 | 121 | // ObjectRef : 122 | // >tests>textfilecontent54_test>object-object_ref 123 | // >tests>uname_test>object-object_ref 124 | // >tests>dpkginfo_test>object-object_ref 125 | type ObjectRef struct { 126 | XMLName xml.Name `xml:"object"` 127 | Text string `xml:",chardata"` 128 | ObjectRef string `xml:"object_ref,attr"` 129 | } 130 | 131 | // StateRef : 132 | // >tests>textfilecontent54_test>state-state_ref 133 | // >tests>dpkginfo_test>state-state_ref 134 | type StateRef struct { 135 | XMLName xml.Name `xml:"state"` 136 | Text string `xml:",chardata"` 137 | StateRef string `xml:"state_ref,attr"` 138 | } 139 | 140 | // Objects : >objects 141 | type Objects struct { 142 | XMLName xml.Name `xml:"objects"` 143 | Textfilecontent54Object Textfilecontent54Object `xml:"textfilecontent54_object"` 144 | UnameObject UnameObject `xml:"uname_object"` 145 | DpkginfoObject []DpkginfoObject `xml:"dpkginfo_object"` 146 | } 147 | 148 | // Textfilecontent54Object : >objects>textfilecontent54_object 149 | type Textfilecontent54Object struct { 150 | ID string `xml:"id,attr"` 151 | Path string `xml:"path"` 152 | Filename string `xml:"filename"` 153 | Pattern struct { 154 | Text string `xml:",chardata"` 155 | Operation string `xml:"operation,attr"` 156 | } `xml:"pattern"` 157 | Instance struct { 158 | Text string `xml:",chardata"` 159 | Datatype string `xml:"datatype,attr"` 160 | } `xml:"instance"` 161 | } 162 | 163 | // UnameObject : >objects>uname_object 164 | type UnameObject struct { 165 | ID string `xml:"id,attr"` 166 | } 167 | 168 | // DpkginfoObject : >objects>dpkginfo_object 169 | type DpkginfoObject struct { 170 | ID string `xml:"id,attr"` 171 | Name string `xml:"name"` 172 | } 173 | 174 | // States : >states 175 | type States struct { 176 | XMLName xml.Name `xml:"states"` 177 | Textfilecontent54State Textfilecontent54State `xml:"textfilecontent54_state"` 178 | DpkginfoState []DpkginfoState `xml:"dpkginfo_state"` 179 | } 180 | 181 | // Textfilecontent54State : >states>textfilecontent54_state 182 | type Textfilecontent54State struct { 183 | ID string `xml:"id,attr"` 184 | Subexpression struct { 185 | Text string `xml:",chardata"` 186 | Operation string `xml:"operation,attr"` 187 | } `xml:"subexpression"` 188 | } 189 | 190 | // DpkginfoState : >states>dpkginfo_state 191 | type DpkginfoState struct { 192 | ID string `xml:"id,attr"` 193 | Evr struct { 194 | Text string `xml:",chardata"` 195 | Datatype string `xml:"datatype,attr"` 196 | Operation string `xml:"operation,attr"` 197 | } `xml:"evr"` 198 | } 199 | -------------------------------------------------------------------------------- /models/ubuntu/ubuntu.go: -------------------------------------------------------------------------------- 1 | package ubuntu 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | "time" 7 | 8 | "github.com/inconshreveable/log15" 9 | "github.com/spf13/viper" 10 | "golang.org/x/xerrors" 11 | 12 | "github.com/vulsio/goval-dictionary/models" 13 | "github.com/vulsio/goval-dictionary/models/util" 14 | ) 15 | 16 | // ConvertToModel Convert OVAL to models 17 | func ConvertToModel(root *Root) ([]models.Definition, error) { 18 | tests, err := parseTests(*root) 19 | if err != nil { 20 | return nil, xerrors.Errorf("Failed to parse oval.Tests. err: %w", err) 21 | } 22 | return parseDefinitions(root.Definitions.Definitions, tests), nil 23 | } 24 | 25 | var rePkgComment = regexp.MustCompile(`The '(.*)' package binar.+`) 26 | 27 | func parseObjects(ovalObjs Objects) map[string]string { 28 | objs := map[string]string{} 29 | for _, obj := range ovalObjs.Textfilecontent54Object { 30 | matched := rePkgComment.FindAllStringSubmatch(obj.Comment, 1) 31 | if len(matched[0]) != 2 { 32 | continue 33 | } 34 | objs[obj.ID] = matched[0][1] 35 | } 36 | return objs 37 | } 38 | 39 | func parseStates(objStates States) map[string]Textfilecontent54State { 40 | states := map[string]Textfilecontent54State{} 41 | for _, state := range objStates.Textfilecontent54State { 42 | states[state.ID] = state 43 | } 44 | return states 45 | } 46 | 47 | func parseTests(root Root) (map[string]dpkgInfoTest, error) { 48 | objs := parseObjects(root.Objects) 49 | states := parseStates(root.States) 50 | tests := map[string]dpkgInfoTest{} 51 | for _, test := range root.Tests.Textfilecontent54Test { 52 | t, err := followTestRefs(test, objs, states) 53 | if err != nil { 54 | return nil, xerrors.Errorf("Failed to follow test refs. err: %w", err) 55 | } 56 | tests[test.ID] = t 57 | } 58 | return tests, nil 59 | } 60 | 61 | func followTestRefs(test Textfilecontent54Test, objects map[string]string, states map[string]Textfilecontent54State) (dpkgInfoTest, error) { 62 | var t dpkgInfoTest 63 | 64 | // Follow object ref 65 | if test.Object.ObjectRef == "" { 66 | return t, nil 67 | } 68 | 69 | pkgName, ok := objects[test.Object.ObjectRef] 70 | if !ok { 71 | return t, xerrors.Errorf("Failed to find object ref. object ref: %s, test ref: %s, err: invalid tests data", test.Object.ObjectRef, test.ID) 72 | } 73 | t.Name = pkgName 74 | 75 | // Follow state ref 76 | if test.State.StateRef == "" { 77 | return t, nil 78 | } 79 | 80 | state, ok := states[test.State.StateRef] 81 | if !ok { 82 | return t, xerrors.Errorf("Failed to find state ref. state ref: %s, test ref: %s, err: invalid tests data", test.State.StateRef, test.ID) 83 | } 84 | 85 | if state.Subexpression.Datatype == "debian_evr_string" && state.Subexpression.Operation == "less than" { 86 | t.FixedVersion = state.Subexpression.Text 87 | } 88 | 89 | return t, nil 90 | } 91 | 92 | func parseDefinitions(ovalDefs []Definition, tests map[string]dpkgInfoTest) []models.Definition { 93 | defs := []models.Definition{} 94 | 95 | for _, d := range ovalDefs { 96 | if strings.Contains(d.Description, "** REJECT **") { 97 | continue 98 | } 99 | 100 | cves := []models.Cve{} 101 | rs := []models.Reference{} 102 | for _, r := range d.References { 103 | if r.Source == "CVE" { 104 | cves = append(cves, models.Cve{ 105 | CveID: r.RefID, 106 | Href: r.RefURL, 107 | }) 108 | } 109 | 110 | rs = append(rs, models.Reference{ 111 | Source: r.Source, 112 | RefID: r.RefID, 113 | RefURL: r.RefURL, 114 | }) 115 | } 116 | 117 | for _, r := range d.Advisory.Refs { 118 | rs = append(rs, models.Reference{ 119 | Source: "Ref", 120 | RefURL: r.URL, 121 | }) 122 | } 123 | 124 | for _, r := range d.Advisory.Bugs { 125 | rs = append(rs, models.Reference{ 126 | Source: "Bug", 127 | RefURL: r.URL, 128 | }) 129 | } 130 | 131 | date := util.ParsedOrDefaultTime([]string{"2006-01-02", "2006-01-02 15:04:05", "2006-01-02 15:04:05 +0000", "2006-01-02 15:04:05 MST"}, d.Advisory.PublicDate) 132 | 133 | def := models.Definition{ 134 | DefinitionID: d.ID, 135 | Title: d.Title, 136 | Description: d.Description, 137 | Advisory: models.Advisory{ 138 | Severity: d.Advisory.Severity, 139 | Cves: cves, 140 | Bugzillas: []models.Bugzilla{}, 141 | AffectedCPEList: []models.Cpe{}, 142 | Issued: date, 143 | Updated: date, 144 | }, 145 | Debian: nil, 146 | AffectedPacks: collectUbuntuPacks(d.Criteria, tests), 147 | References: rs, 148 | } 149 | 150 | if viper.GetBool("no-details") { 151 | def.Title = "" 152 | def.Description = "" 153 | def.Advisory.Severity = "" 154 | def.Advisory.AffectedCPEList = []models.Cpe{} 155 | def.Advisory.Bugzillas = []models.Bugzilla{} 156 | def.Advisory.Issued = time.Time{} 157 | def.Advisory.Updated = time.Time{} 158 | def.References = []models.Reference{} 159 | } 160 | 161 | defs = append(defs, def) 162 | } 163 | 164 | return defs 165 | } 166 | 167 | func collectUbuntuPacks(cri Criteria, tests map[string]dpkgInfoTest) []models.Package { 168 | return walkCriterion(cri, tests) 169 | } 170 | 171 | func walkCriterion(cri Criteria, tests map[string]dpkgInfoTest) []models.Package { 172 | pkgs := []models.Package{} 173 | for _, c := range cri.Criterions { 174 | t, ok := tests[c.TestRef] 175 | if !ok { 176 | continue 177 | } 178 | 179 | if strings.Contains(c.Comment, "is related to the CVE in some way and has been fixed") || // status: not vulnerable(= not affected) 180 | strings.Contains(c.Comment, "is affected and may need fixing") { // status: needs-triage 181 | continue 182 | } 183 | 184 | if strings.Contains(c.Comment, "is affected and needs fixing") || // status: needed 185 | strings.Contains(c.Comment, "is affected, but a decision has been made to defer addressing it") || // status: deferred 186 | strings.Contains(c.Comment, "is affected. An update containing the fix has been completed and is pending publication") || // status: pending 187 | strings.Contains(c.Comment, "while related to the CVE in some way, a decision has been made to ignore this issue") { // status: ignored 188 | pkgs = append(pkgs, models.Package{ 189 | Name: t.Name, 190 | NotFixedYet: true, 191 | }) 192 | } else if strings.Contains(c.Comment, "was vulnerable but has been fixed") || // status: released 193 | strings.Contains(c.Comment, "was vulnerable and has been fixed") { // status: released, only this comment: "firefox package in $RELEASE_NAME was vulnerable and has been fixed, but no release version available for it." 194 | pkgs = append(pkgs, models.Package{ 195 | Name: t.Name, 196 | Version: t.FixedVersion, 197 | NotFixedYet: false, 198 | }) 199 | } else { 200 | log15.Warn("Failed to detect patch status.", "comment", c.Comment) 201 | } 202 | } 203 | 204 | for _, c := range cri.Criterias { 205 | if ps := walkCriterion(c, tests); len(ps) > 0 { 206 | pkgs = append(pkgs, ps...) 207 | } 208 | } 209 | return pkgs 210 | } 211 | -------------------------------------------------------------------------------- /fetcher/fedora/types_test.go: -------------------------------------------------------------------------------- 1 | package fedora 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/google/go-cmp/cmp" 8 | "github.com/google/go-cmp/cmp/cmpopts" 9 | 10 | models "github.com/vulsio/goval-dictionary/models/fedora" 11 | ) 12 | 13 | func TestRpmNewPackageFromRpm(t *testing.T) { 14 | tests := []struct { 15 | name string 16 | rpm Rpm 17 | want models.Package 18 | wantErr bool 19 | }{ 20 | { 21 | name: "normal", 22 | rpm: "name-1:1.0-1.module_12345.aarch64", 23 | want: models.Package{ 24 | Name: "name", 25 | Epoch: "1", 26 | Version: "1.0", 27 | Release: "1.module_12345", 28 | Arch: "aarch64", 29 | Filename: "name-1:1.0-1.module_12345.aarch64", 30 | }, 31 | }, 32 | { 33 | name: "with.rpm", 34 | rpm: "name-1:1.0-1.module_12345.aarch64.rpm", 35 | want: models.Package{ 36 | Name: "name", 37 | Epoch: "1", 38 | Version: "1.0", 39 | Release: "1.module_12345", 40 | Arch: "aarch64", 41 | Filename: "name-1:1.0-1.module_12345.aarch64", 42 | }, 43 | }, 44 | { 45 | name: "name-with-hyphen", 46 | rpm: "name-with-hyphen-1:1.0-1.module_12345.aarch64", 47 | want: models.Package{ 48 | Name: "name-with-hyphen", 49 | Epoch: "1", 50 | Version: "1.0", 51 | Release: "1.module_12345", 52 | Arch: "aarch64", 53 | Filename: "name-with-hyphen-1:1.0-1.module_12345.aarch64", 54 | }, 55 | }, 56 | { 57 | name: "invalid rpm", 58 | rpm: "invalid rpm", 59 | wantErr: true, 60 | }, 61 | { 62 | name: "can not find release", 63 | rpm: "no_release:1.0.aarch64", 64 | wantErr: true, 65 | }, 66 | { 67 | name: "can not find version", 68 | rpm: "no_version:1.0-1.module_12345.aarch64", 69 | wantErr: true, 70 | }, 71 | { 72 | name: "can not find epoch", 73 | rpm: "noepoch-1.0-1.module_12345.aarch64", 74 | want: models.Package{ 75 | Name: "noepoch", 76 | Epoch: "0", 77 | Version: "1.0", 78 | Release: "1.module_12345", 79 | Arch: "aarch64", 80 | Filename: "noepoch-1.0-1.module_12345.aarch64", 81 | }, 82 | }, 83 | } 84 | for _, tt := range tests { 85 | t.Run(tt.name, func(t *testing.T) { 86 | got, err := tt.rpm.NewPackageFromRpm() 87 | if (err == nil) == tt.wantErr { 88 | t.Fatalf("unexpected error: %v", err) 89 | } 90 | 91 | if diff := cmp.Diff(got, tt.want); diff != "" { 92 | t.Errorf("(-got +want):\n%s", diff) 93 | } 94 | }) 95 | } 96 | } 97 | 98 | func TestUpdatesPerVersionMerge(t *testing.T) { 99 | tests := []struct { 100 | name string 101 | source map[string]*models.Updates 102 | target map[string]*models.Updates 103 | want map[string]*models.Updates 104 | }{ 105 | { 106 | name: "merge success", 107 | source: map[string]*models.Updates{ 108 | "35": { 109 | UpdateList: []models.UpdateInfo{ 110 | { 111 | Title: "update35-1", 112 | }, 113 | { 114 | Title: "update35-2", 115 | }, 116 | }, 117 | }, 118 | "34": { 119 | UpdateList: []models.UpdateInfo{ 120 | { 121 | Title: "update34-1", 122 | }, 123 | }, 124 | }, 125 | }, 126 | target: map[string]*models.Updates{ 127 | "35": { 128 | UpdateList: []models.UpdateInfo{ 129 | { 130 | Title: "update35-module-1", 131 | }, 132 | }, 133 | }, 134 | "34": { 135 | UpdateList: []models.UpdateInfo{ 136 | { 137 | Title: "update34-module-1", 138 | }, 139 | }, 140 | }, 141 | }, 142 | want: map[string]*models.Updates{ 143 | "35": { 144 | UpdateList: []models.UpdateInfo{ 145 | { 146 | Title: "update35-1", 147 | }, 148 | { 149 | Title: "update35-2", 150 | }, 151 | { 152 | Title: "update35-module-1", 153 | }, 154 | }, 155 | }, 156 | "34": { 157 | UpdateList: []models.UpdateInfo{ 158 | { 159 | Title: "update34-1", 160 | }, 161 | { 162 | Title: "update34-module-1", 163 | }, 164 | }, 165 | }, 166 | }, 167 | }, 168 | { 169 | name: "no panic when some version is missing", 170 | source: map[string]*models.Updates{ 171 | "35": { 172 | UpdateList: []models.UpdateInfo{ 173 | { 174 | Title: "update35-1", 175 | }, 176 | { 177 | Title: "update35-2", 178 | }, 179 | }, 180 | }, 181 | "34": { 182 | UpdateList: []models.UpdateInfo{ 183 | { 184 | Title: "update34-1", 185 | }, 186 | }, 187 | }, 188 | }, 189 | target: map[string]*models.Updates{ 190 | "35": { 191 | UpdateList: []models.UpdateInfo{ 192 | { 193 | Title: "update35-module-1", 194 | }, 195 | }, 196 | }, 197 | }, 198 | want: map[string]*models.Updates{ 199 | "35": { 200 | UpdateList: []models.UpdateInfo{ 201 | { 202 | Title: "update35-1", 203 | }, 204 | { 205 | Title: "update35-2", 206 | }, 207 | { 208 | Title: "update35-module-1", 209 | }, 210 | }, 211 | }, 212 | "34": { 213 | UpdateList: []models.UpdateInfo{ 214 | { 215 | Title: "update34-1", 216 | }, 217 | }, 218 | }, 219 | }, 220 | }, 221 | { 222 | name: "target is nil", 223 | source: map[string]*models.Updates{ 224 | "39": { 225 | UpdateList: []models.UpdateInfo{ 226 | { 227 | Title: "update39", 228 | }, 229 | }, 230 | }, 231 | }, 232 | target: nil, 233 | want: map[string]*models.Updates{ 234 | "39": { 235 | UpdateList: []models.UpdateInfo{ 236 | { 237 | Title: "update39", 238 | }, 239 | }, 240 | }, 241 | }, 242 | }, 243 | } 244 | for _, tt := range tests { 245 | t.Run(tt.name, func(t *testing.T) { 246 | got := mergeUpdates(tt.source, tt.target) 247 | if diff := cmp.Diff(got, tt.want); diff != "" { 248 | t.Errorf("(-got +want):\n%s", diff) 249 | } 250 | }) 251 | } 252 | } 253 | 254 | func TestUniquePackages(t *testing.T) { 255 | opt := cmpopts.SortSlices((func(x, y models.Package) bool { return strings.Compare(x.Filename, y.Filename) > 0 })) 256 | tests := []struct { 257 | name string 258 | in []models.Package 259 | want []models.Package 260 | }{ 261 | { 262 | name: "normal", 263 | in: []models.Package{ 264 | {Filename: "package1"}, 265 | {Filename: "package2"}, 266 | {Filename: "package2"}, 267 | {Filename: "package3"}, 268 | {Filename: "package3"}, 269 | {Filename: "package3"}, 270 | }, 271 | want: []models.Package{ 272 | {Filename: "package1"}, 273 | {Filename: "package2"}, 274 | {Filename: "package3"}, 275 | }, 276 | }, 277 | { 278 | name: "no panic when it is blank", 279 | in: []models.Package{}, 280 | want: []models.Package{}, 281 | }, 282 | } 283 | for _, tt := range tests { 284 | t.Run(tt.name, func(t *testing.T) { 285 | got := uniquePackages(tt.in) 286 | if diff := cmp.Diff(got, tt.want, opt); diff != "" { 287 | t.Errorf("(-got +want):\n%s", diff) 288 | } 289 | }) 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /fetcher/amazon/amazon.go: -------------------------------------------------------------------------------- 1 | package amazon 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "compress/gzip" 7 | "encoding/json" 8 | "encoding/xml" 9 | "errors" 10 | "fmt" 11 | "net/url" 12 | "path" 13 | 14 | "github.com/inconshreveable/log15" 15 | "golang.org/x/xerrors" 16 | 17 | "github.com/vulsio/goval-dictionary/fetcher/util" 18 | models "github.com/vulsio/goval-dictionary/models/amazon" 19 | ) 20 | 21 | // updateinfo for x86_64 also contains information for aarch64 22 | 23 | type mirror struct { 24 | core string 25 | extra string 26 | livepatch string 27 | } 28 | 29 | var mirrors = map[string]mirror{ 30 | "1": {core: "http://repo.us-west-2.amazonaws.com/2018.03/updates/x86_64/mirror.list"}, 31 | "2": { 32 | core: "https://cdn.amazonlinux.com/2/core/latest/x86_64/mirror.list", 33 | extra: "http://amazonlinux.default.amazonaws.com/2/extras-catalog.json", 34 | }, 35 | "2022": { 36 | core: "https://cdn.amazonlinux.com/al2022/core/mirrors/latest/x86_64/mirror.list", 37 | }, 38 | "2023": { 39 | core: "https://cdn.amazonlinux.com/al2023/core/mirrors/latest/x86_64/mirror.list", 40 | livepatch: "https://cdn.amazonlinux.com/al2023/kernel-livepatch/mirrors/latest/x86_64/mirror.list", 41 | }, 42 | } 43 | 44 | var errNoUpdateInfo = xerrors.New("No updateinfo field in the repomd") 45 | 46 | // FetchFiles fetch from Amazon ALAS 47 | func FetchFiles(versions []string) (map[string]*models.Updates, error) { 48 | m := map[string]*models.Updates{} 49 | for _, v := range versions { 50 | switch v { 51 | case "1", "2022": 52 | us, err := fetchUpdateInfoAmazonLinux(mirrors[v].core) 53 | if err != nil { 54 | return nil, xerrors.Errorf("Failed to fetch Amazon Linux %s UpdateInfo. err: %w", v, err) 55 | } 56 | m[v] = us 57 | case "2": 58 | updates, err := fetchUpdateInfoAmazonLinux(mirrors[v].core) 59 | if err != nil { 60 | return nil, xerrors.Errorf("Failed to fetch Amazon Linux %s UpdateInfo. err: %w", v, err) 61 | } 62 | 63 | rs, err := util.FetchFeedFiles([]util.FetchRequest{{URL: mirrors[v].extra, MIMEType: util.MIMETypeJSON}}) 64 | if err != nil || len(rs) != 1 { 65 | return nil, xerrors.Errorf("Failed to fetch extras-catalog.json for Amazon Linux 2. url: %s, err: %w", mirrors[v].extra, err) 66 | } 67 | 68 | var catalog extrasCatalog 69 | if err := json.Unmarshal(rs[0].Body, &catalog); err != nil { 70 | return nil, xerrors.Errorf("Failed to unmarshal extras-catalog.json for Amazon Linux 2. err: %w", err) 71 | } 72 | 73 | for _, t := range catalog.Topics { 74 | us, err := fetchUpdateInfoAmazonLinux(fmt.Sprintf("https://cdn.amazonlinux.com/2/extras/%s/latest/x86_64/mirror.list", t.N)) 75 | if err != nil { 76 | if errors.Is(err, errNoUpdateInfo) { 77 | continue 78 | } 79 | return nil, xerrors.Errorf("Failed to fetch Amazon Linux 2 %s updateinfo. err: %w", t.N, err) 80 | } 81 | for _, u := range us.UpdateList { 82 | u.Repository = fmt.Sprintf("amzn2extra-%s", t.N) 83 | updates.UpdateList = append(updates.UpdateList, u) 84 | } 85 | } 86 | 87 | m[v] = updates 88 | case "2023": 89 | updates, err := fetchUpdateInfoAmazonLinux(mirrors[v].core) 90 | if err != nil { 91 | return nil, xerrors.Errorf("Failed to fetch Amazon Linux %s UpdateInfo. err: %w", v, err) 92 | } 93 | 94 | us, err := fetchUpdateInfoAmazonLinux(mirrors[v].livepatch) 95 | if err != nil { 96 | return nil, xerrors.Errorf("Failed to fetch Amazon Linux %s Kernel Livepatch UpdateInfo. err: %w", v, err) 97 | } 98 | 99 | for _, u := range us.UpdateList { 100 | u.Repository = "kernel-livepatch" 101 | updates.UpdateList = append(updates.UpdateList, u) 102 | } 103 | 104 | m[v] = updates 105 | default: 106 | log15.Warn("Skip unknown amazon.", "version", v) 107 | } 108 | } 109 | return m, nil 110 | } 111 | 112 | func fetchUpdateInfoAmazonLinux(mirrorListURL string) (uinfo *models.Updates, err error) { 113 | results, err := util.FetchFeedFiles([]util.FetchRequest{{URL: mirrorListURL, MIMEType: util.MIMETypeXML}}) 114 | if err != nil || len(results) != 1 { 115 | return nil, xerrors.Errorf("Failed to fetch mirror list files. err: %w", err) 116 | } 117 | 118 | mirrors := []string{} 119 | for _, r := range results { 120 | scanner := bufio.NewScanner(bytes.NewReader(r.Body)) 121 | for scanner.Scan() { 122 | mirrors = append(mirrors, scanner.Text()) 123 | } 124 | } 125 | 126 | uinfoURLs, err := fetchUpdateInfoURL(mirrors) 127 | if err != nil { 128 | return nil, xerrors.Errorf("Failed to fetch updateInfo URL. err: %w", err) 129 | } 130 | for _, url := range uinfoURLs { 131 | uinfo, err = fetchUpdateInfo(url) 132 | if err != nil { 133 | log15.Warn("Failed to fetch updateinfo. continue with other mirror", "err", err) 134 | continue 135 | } 136 | return uinfo, nil 137 | } 138 | return nil, xerrors.New("Failed to fetch updateinfo") 139 | } 140 | 141 | // FetchUpdateInfoURL fetches update info urls for AmazonLinux1 ,Amazon Linux2 and Amazon Linux2022. 142 | func fetchUpdateInfoURL(mirrors []string) (updateInfoURLs []string, err error) { 143 | reqs := []util.FetchRequest{} 144 | for _, mirror := range mirrors { 145 | u, err := url.Parse(mirror) 146 | if err != nil { 147 | return nil, err 148 | } 149 | u.Path = path.Join(u.Path, "/repodata/repomd.xml") 150 | reqs = append(reqs, util.FetchRequest{ 151 | Target: mirror, // base URL of the mirror site 152 | URL: u.String(), 153 | MIMEType: util.MIMETypeXML, 154 | }) 155 | } 156 | 157 | results, err := util.FetchFeedFiles(reqs) 158 | if err != nil { 159 | log15.Warn("Some errors occurred while fetching repomd", "err", err) 160 | } 161 | if len(results) == 0 { 162 | return nil, xerrors.Errorf("Failed to fetch repomd.xml. URLs: %s", mirrors) 163 | } 164 | 165 | for _, r := range results { 166 | var repoMd repoMd 167 | if err := xml.NewDecoder(bytes.NewBuffer(r.Body)).Decode(&repoMd); err != nil { 168 | log15.Warn("Failed to decode repomd. Trying another mirror", "err", err) 169 | continue 170 | } 171 | 172 | for _, repo := range repoMd.RepoList { 173 | if repo.Type == "updateinfo" { 174 | u, err := url.Parse(r.Target) 175 | if err != nil { 176 | return nil, err 177 | } 178 | u.Path = path.Join(u.Path, repo.Location.Href) 179 | updateInfoURLs = append(updateInfoURLs, u.String()) 180 | break 181 | } 182 | } 183 | } 184 | if len(updateInfoURLs) == 0 { 185 | return nil, errNoUpdateInfo 186 | } 187 | return updateInfoURLs, nil 188 | } 189 | 190 | func fetchUpdateInfo(url string) (*models.Updates, error) { 191 | results, err := util.FetchFeedFiles([]util.FetchRequest{{URL: url, MIMEType: util.MIMETypeXML}}) 192 | if err != nil || len(results) != 1 { 193 | return nil, xerrors.Errorf("Failed to fetch updateInfo. err: %w", err) 194 | } 195 | r, err := gzip.NewReader(bytes.NewBuffer(results[0].Body)) 196 | if err != nil { 197 | return nil, xerrors.Errorf("Failed to decompress updateInfo. err: %w", err) 198 | } 199 | defer r.Close() 200 | 201 | var updateInfo models.Updates 202 | if err := xml.NewDecoder(r).Decode(&updateInfo); err != nil { 203 | return nil, err 204 | } 205 | for i, alas := range updateInfo.UpdateList { 206 | cveIDs := []string{} 207 | for _, ref := range alas.References { 208 | if ref.Type == "cve" { 209 | cveIDs = append(cveIDs, ref.ID) 210 | } 211 | } 212 | updateInfo.UpdateList[i].CVEIDs = cveIDs 213 | } 214 | return &updateInfo, nil 215 | } 216 | -------------------------------------------------------------------------------- /models/redhat/redhat_test.go: -------------------------------------------------------------------------------- 1 | package redhat 2 | 3 | import ( 4 | "reflect" 5 | "sort" 6 | "testing" 7 | 8 | "github.com/k0kubun/pp" 9 | 10 | "github.com/vulsio/goval-dictionary/models" 11 | ) 12 | 13 | func TestWalkRedHat(t *testing.T) { 14 | var tests = []struct { 15 | version string 16 | cri Criteria 17 | expected []models.Package 18 | }{ 19 | { 20 | version: "6", 21 | cri: Criteria{ 22 | Criterions: []Criterion{ 23 | {Comment: "kernel-headers is earlier than 0:2.6.32-71.7.1.el6"}, 24 | }, 25 | }, 26 | expected: []models.Package{ 27 | { 28 | Name: "kernel-headers", 29 | Version: "0:2.6.32-71.7.1.el6", 30 | }, 31 | }, 32 | }, 33 | { 34 | version: "6", 35 | cri: Criteria{ 36 | Criterias: []Criteria{ 37 | { 38 | Criterions: []Criterion{ 39 | {Comment: "kernel-headers is earlier than 0:2.6.32-71.7.1.el6"}, 40 | {Comment: "kernel-headers is signed with Red Hat redhatrelease2 key"}, 41 | }, 42 | }, 43 | }, 44 | Criterions: []Criterion{ 45 | {Comment: "kernel-kdump is signed with Red Hat redhatrelease2 key"}, 46 | {Comment: "kernel-kdump is earlier than 0:2.6.32-71.7.1.el6"}, 47 | }, 48 | }, 49 | expected: []models.Package{ 50 | { 51 | Name: "kernel-headers", 52 | Version: "0:2.6.32-71.7.1.el6", 53 | }, 54 | { 55 | Name: "kernel-kdump", 56 | Version: "0:2.6.32-71.7.1.el6", 57 | }, 58 | }, 59 | }, 60 | { 61 | version: "6", 62 | cri: Criteria{ 63 | Criterias: []Criteria{ 64 | { 65 | Criterions: []Criterion{ 66 | {Comment: "bzip2 is earlier than 0:1.0.5-7.el6_0"}, 67 | {Comment: "bzip2 is signed with Red Hat redhatrelease2 key"}, 68 | }, 69 | 70 | Criterias: []Criteria{ 71 | { 72 | Criterions: []Criterion{ 73 | {Comment: "samba-domainjoin-gui is earlier than 0:3.5.4-68.el6_0.1"}, 74 | {Comment: "samba-domainjoin-gui is signed with Red Hat redhatrelease2 key"}, 75 | }, 76 | }, 77 | }, 78 | }, 79 | { 80 | Criterions: []Criterion{ 81 | {Comment: "poppler-qt4 is signed with Red Hat redhatrelease2 key"}, 82 | {Comment: "poppler-qt4 is earlier than 0:0.12.4-3.el6_0.1"}, 83 | }, 84 | }, 85 | }, 86 | Criterions: []Criterion{ 87 | {Comment: "kernel-kdump is earlier than 0:2.6.32-71.7.1.el6"}, 88 | {Comment: "kernel-kdump is signed with Red Hat redhatrelease2 key"}, 89 | }, 90 | }, 91 | expected: []models.Package{ 92 | { 93 | Name: "bzip2", 94 | Version: "0:1.0.5-7.el6_0", 95 | }, 96 | { 97 | Name: "samba-domainjoin-gui", 98 | Version: "0:3.5.4-68.el6_0.1", 99 | }, 100 | { 101 | Name: "poppler-qt4", 102 | Version: "0:0.12.4-3.el6_0.1", 103 | }, 104 | { 105 | Name: "kernel-kdump", 106 | Version: "0:2.6.32-71.7.1.el6", 107 | }, 108 | }, 109 | }, 110 | { 111 | version: "6", 112 | cri: Criteria{ 113 | Criterias: []Criteria{ 114 | { 115 | Criterias: []Criteria{ 116 | { 117 | Criterions: []Criterion{ 118 | {Comment: "rpm is earlier than 0:4.8.0-12.el6_0.2"}, 119 | }, 120 | }, 121 | }, 122 | Criterions: []Criterion{ 123 | {Comment: "Red Hat Enterprise Linux 6 is installed"}, 124 | }, 125 | }, 126 | { 127 | Criterias: []Criteria{ 128 | { 129 | Criterions: []Criterion{ 130 | {Comment: "rpm is earlier than 0:4.8.0-19.el6_2.1"}, 131 | }, 132 | }, 133 | }, 134 | Criterions: []Criterion{ 135 | {Comment: "Red Hat Enterprise Linux 6 is installed"}, 136 | }, 137 | }, 138 | }, 139 | }, 140 | expected: []models.Package{ 141 | { 142 | Name: "rpm", 143 | Version: "0:4.8.0-19.el6_2.1", 144 | }, 145 | }, 146 | }, 147 | { 148 | version: "6", 149 | cri: Criteria{ 150 | Criterias: []Criteria{ 151 | { 152 | Criterias: []Criteria{ 153 | { 154 | Criterions: []Criterion{ 155 | {Comment: "rpm is earlier than 0:4.8.0-12.el6_0.2"}, 156 | }, 157 | }, 158 | }, 159 | Criterions: []Criterion{ 160 | {Comment: "Red Hat Enterprise Linux 6 is installed"}, 161 | }, 162 | }, 163 | { 164 | Criterias: []Criteria{ 165 | { 166 | Criterions: []Criterion{ 167 | {Comment: "rpm is earlier than 0:4.8.0-19.el7_0.1"}, 168 | }, 169 | }, 170 | }, 171 | Criterions: []Criterion{ 172 | {Comment: "Red Hat Enterprise Linux 7 is installed"}, 173 | }, 174 | }, 175 | }, 176 | }, 177 | expected: []models.Package{ 178 | { 179 | Name: "rpm", 180 | Version: "0:4.8.0-12.el6_0.2", 181 | }, 182 | }, 183 | }, 184 | { 185 | version: "8", 186 | cri: Criteria{ 187 | Criterias: []Criteria{ 188 | { 189 | Criterions: []Criterion{ 190 | {Comment: "ruby is earlier than 0:2.5.5-105.module+el8.1.0+3656+f80bfa1d"}, 191 | {Comment: "ruby is signed with Red Hat redhatrelease2 key"}, 192 | }, 193 | }, 194 | }, 195 | Criterions: []Criterion{ 196 | {Comment: "Red Hat Enterprise Linux 8 is installed"}, 197 | {Comment: "Module ruby:2.5 is enabled"}, 198 | }, 199 | }, 200 | expected: []models.Package{ 201 | { 202 | Name: "ruby", 203 | Version: "0:2.5.5-105.module+el8.1.0+3656+f80bfa1d", 204 | ModularityLabel: "ruby:2.5", 205 | }, 206 | }, 207 | }, 208 | { 209 | version: "8", 210 | cri: Criteria{ 211 | Criterias: []Criteria{ 212 | { 213 | Criterias: []Criteria{ 214 | { 215 | Criterions: []Criterion{ 216 | {Comment: "libvirt is earlier than 0:4.5.0-42.module+el8.2.0+6024+15a2423f"}, 217 | {Comment: "libvirt is signed with Red Hat redhatrelease2 key"}, 218 | }, 219 | }, 220 | }, 221 | Criterions: []Criterion{ 222 | {Comment: "Module virt:rhel is enabled"}, 223 | }, 224 | }, 225 | { 226 | Criterias: []Criteria{ 227 | { 228 | Criterions: []Criterion{ 229 | {Comment: "libvirt is earlier than 0:4.5.0-42.module+el8.2.0+6024+15a2423f"}, 230 | {Comment: "libvirt is signed with Red Hat redhatrelease2 key"}, 231 | }, 232 | }, 233 | }, 234 | Criterions: []Criterion{ 235 | {Comment: "Module virt-devel:rhel is enabled"}, 236 | }, 237 | }, 238 | }, 239 | Criterions: []Criterion{ 240 | {Comment: "Red Hat Enterprise Linux 8 is installed"}, 241 | }, 242 | }, 243 | expected: []models.Package{ 244 | { 245 | Name: "libvirt", 246 | Version: "0:4.5.0-42.module+el8.2.0+6024+15a2423f", 247 | ModularityLabel: "virt:rhel", 248 | }, 249 | { 250 | Name: "libvirt", 251 | Version: "0:4.5.0-42.module+el8.2.0+6024+15a2423f", 252 | ModularityLabel: "virt-devel:rhel", 253 | }, 254 | }, 255 | }, 256 | { 257 | version: "8", 258 | cri: Criteria{ 259 | Criterias: []Criteria{ 260 | { 261 | Criterias: []Criteria{ 262 | { 263 | Criterias: []Criteria{ 264 | { 265 | Criterias: []Criteria{ 266 | { 267 | Criterions: []Criterion{ 268 | {Comment: "python2 is installed"}, 269 | {Comment: "python2 is signed with Red Hat redhatrelease2 key"}, 270 | }, 271 | }, 272 | }, 273 | }, 274 | }, 275 | Criterions: []Criterion{ 276 | {Comment: "Module inkscape:flatpak is enabled"}, 277 | }, 278 | }, 279 | }, 280 | Criterions: []Criterion{ 281 | {Comment: "Red Hat Enterprise Linux 8 is installed"}, 282 | {Comment: "Red Hat CoreOS 4 is installed"}, 283 | }, 284 | }, 285 | }, 286 | Criterions: []Criterion{ 287 | {Comment: "Red Hat Enterprise Linux must be installed"}, 288 | }, 289 | }, 290 | expected: []models.Package{ 291 | { 292 | Name: "python2", 293 | ModularityLabel: "inkscape:flatpak", 294 | NotFixedYet: true, 295 | }, 296 | }, 297 | }, 298 | } 299 | 300 | for i, tt := range tests { 301 | actual := collectRedHatPacks(tt.version, tt.cri) 302 | sort.Slice(actual, func(i, j int) bool { 303 | if actual[i].Name == actual[j].Name { 304 | return actual[i].ModularityLabel < actual[j].ModularityLabel 305 | } 306 | return actual[i].Name < actual[j].Name 307 | }) 308 | sort.Slice(tt.expected, func(i, j int) bool { 309 | if tt.expected[i].Name == tt.expected[j].Name { 310 | return tt.expected[i].ModularityLabel < tt.expected[j].ModularityLabel 311 | } 312 | return tt.expected[i].Name < tt.expected[j].Name 313 | }) 314 | 315 | if !reflect.DeepEqual(tt.expected, actual) { 316 | e := pp.Sprintf("%v", tt.expected) 317 | a := pp.Sprintf("%v", actual) 318 | t.Errorf("[%d]: expected: %s\n, actual: %s\n", i, e, a) 319 | } 320 | } 321 | } 322 | -------------------------------------------------------------------------------- /db/db_test.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/vulsio/goval-dictionary/config" 8 | "github.com/vulsio/goval-dictionary/models" 9 | ) 10 | 11 | func Test_formatFamilyAndOSVer(t *testing.T) { 12 | type args struct { 13 | family string 14 | osVer string 15 | } 16 | tests := []struct { 17 | in args 18 | expected args 19 | wantErr string 20 | }{ 21 | { 22 | in: args{ 23 | family: config.Debian, 24 | osVer: "11", 25 | }, 26 | expected: args{ 27 | family: config.Debian, 28 | osVer: "11", 29 | }, 30 | }, 31 | { 32 | in: args{ 33 | family: config.Debian, 34 | osVer: "11.1", 35 | }, 36 | expected: args{ 37 | family: config.Debian, 38 | osVer: "11", 39 | }, 40 | }, 41 | { 42 | in: args{ 43 | family: config.Ubuntu, 44 | osVer: "20.04", 45 | }, 46 | expected: args{ 47 | family: config.Ubuntu, 48 | osVer: "20.04", 49 | }, 50 | }, 51 | { 52 | in: args{ 53 | family: config.Ubuntu, 54 | osVer: "20.04.3", 55 | }, 56 | expected: args{ 57 | family: config.Ubuntu, 58 | osVer: "20.04", 59 | }, 60 | }, 61 | { 62 | in: args{ 63 | family: config.Raspbian, 64 | osVer: "10", 65 | }, 66 | expected: args{ 67 | family: config.Debian, 68 | osVer: "10", 69 | }, 70 | }, 71 | { 72 | in: args{ 73 | family: config.Raspbian, 74 | osVer: "10.1", 75 | }, 76 | expected: args{ 77 | family: config.Debian, 78 | osVer: "10", 79 | }, 80 | }, 81 | { 82 | in: args{ 83 | family: config.RedHat, 84 | osVer: "8", 85 | }, 86 | expected: args{ 87 | family: config.RedHat, 88 | osVer: "8", 89 | }, 90 | }, 91 | { 92 | in: args{ 93 | family: config.RedHat, 94 | osVer: "8.4", 95 | }, 96 | expected: args{ 97 | family: config.RedHat, 98 | osVer: "8", 99 | }, 100 | }, 101 | { 102 | in: args{ 103 | family: config.CentOS, 104 | osVer: "8", 105 | }, 106 | expected: args{ 107 | family: config.RedHat, 108 | osVer: "8", 109 | }, 110 | }, 111 | { 112 | in: args{ 113 | family: config.CentOS, 114 | osVer: "8.4", 115 | }, 116 | expected: args{ 117 | family: config.RedHat, 118 | osVer: "8", 119 | }, 120 | }, 121 | { 122 | in: args{ 123 | family: config.Oracle, 124 | osVer: "8", 125 | }, 126 | expected: args{ 127 | family: config.Oracle, 128 | osVer: "8", 129 | }, 130 | }, 131 | { 132 | in: args{ 133 | family: config.Oracle, 134 | osVer: "8.4", 135 | }, 136 | expected: args{ 137 | family: config.Oracle, 138 | osVer: "8", 139 | }, 140 | }, 141 | { 142 | in: args{ 143 | family: config.Amazon, 144 | osVer: "1", 145 | }, 146 | expected: args{ 147 | family: config.Amazon, 148 | osVer: "1", 149 | }, 150 | }, 151 | { 152 | in: args{ 153 | family: config.Amazon, 154 | osVer: "2", 155 | }, 156 | expected: args{ 157 | family: config.Amazon, 158 | osVer: "2", 159 | }, 160 | }, 161 | { 162 | in: args{ 163 | family: config.Amazon, 164 | osVer: "2022", 165 | }, 166 | expected: args{ 167 | family: config.Amazon, 168 | osVer: "2022", 169 | }, 170 | }, 171 | { 172 | in: args{ 173 | family: config.Amazon, 174 | osVer: "2023", 175 | }, 176 | expected: args{ 177 | family: config.Amazon, 178 | osVer: "2023", 179 | }, 180 | }, 181 | { 182 | in: args{ 183 | family: config.Alpine, 184 | osVer: "3.15", 185 | }, 186 | expected: args{ 187 | family: config.Alpine, 188 | osVer: "3.15", 189 | }, 190 | }, 191 | { 192 | in: args{ 193 | family: config.Alpine, 194 | osVer: "3.14", 195 | }, 196 | expected: args{ 197 | family: config.Alpine, 198 | osVer: "3.14", 199 | }, 200 | }, 201 | { 202 | in: args{ 203 | family: config.Alpine, 204 | osVer: "3.14.1", 205 | }, 206 | expected: args{ 207 | family: config.Alpine, 208 | osVer: "3.14", 209 | }, 210 | }, 211 | { 212 | in: args{ 213 | family: config.OpenSUSE, 214 | osVer: "10.2", 215 | }, 216 | expected: args{ 217 | family: config.OpenSUSE, 218 | osVer: "10.2", 219 | }, 220 | }, 221 | { 222 | in: args{ 223 | family: config.OpenSUSE, 224 | osVer: "tumbleweed", 225 | }, 226 | expected: args{ 227 | family: config.OpenSUSE, 228 | osVer: "tumbleweed", 229 | }, 230 | }, 231 | { 232 | in: args{ 233 | family: config.OpenSUSELeap, 234 | osVer: "15.3", 235 | }, 236 | expected: args{ 237 | family: config.OpenSUSELeap, 238 | osVer: "15.3", 239 | }, 240 | }, 241 | { 242 | in: args{ 243 | family: config.SUSEEnterpriseServer, 244 | osVer: "15", 245 | }, 246 | expected: args{ 247 | family: config.SUSEEnterpriseServer, 248 | osVer: "15", 249 | }, 250 | }, 251 | { 252 | in: args{ 253 | family: config.SUSEEnterpriseDesktop, 254 | osVer: "15", 255 | }, 256 | expected: args{ 257 | family: config.SUSEEnterpriseDesktop, 258 | osVer: "15", 259 | }, 260 | }, 261 | { 262 | in: args{ 263 | family: config.Fedora, 264 | osVer: "35", 265 | }, 266 | expected: args{ 267 | family: config.Fedora, 268 | osVer: "35", 269 | }, 270 | }, 271 | { 272 | in: args{ 273 | family: "unknown", 274 | osVer: "unknown", 275 | }, 276 | wantErr: "Failed to detect family. err: unknown os family(unknown)", 277 | }, 278 | } 279 | for i, tt := range tests { 280 | family, osVer, err := formatFamilyAndOSVer(tt.in.family, tt.in.osVer) 281 | if tt.wantErr != "" { 282 | if err.Error() != tt.wantErr { 283 | t.Errorf("[%d] formatFamilyAndOSVer expected: %#v\n actual: %#v\n", i, tt.wantErr, err) 284 | } 285 | } 286 | 287 | if family != tt.expected.family || osVer != tt.expected.osVer { 288 | t.Errorf("[%d] formatFamilyAndOSVer expected: %#v\n actual: %#v\n", i, tt.expected, args{family: family, osVer: osVer}) 289 | } 290 | } 291 | } 292 | 293 | func Test_filterByRedHatMajor(t *testing.T) { 294 | type args struct { 295 | packs []models.Package 296 | majorVer string 297 | } 298 | tests := []struct { 299 | in args 300 | expected []models.Package 301 | }{ 302 | { 303 | in: args{ 304 | packs: []models.Package{ 305 | { 306 | Name: "name-el7", 307 | Version: "0:0.0.1-0.0.1.el7", 308 | }, 309 | { 310 | Name: "name-el8", 311 | Version: "0:0.0.1-0.0.1.el8", 312 | }, 313 | { 314 | Name: "name-module+el7", 315 | Version: "0:0.1.1-1.module+el7.1.0+7785+0ea9f177", 316 | }, 317 | { 318 | Name: "name-module+el8", 319 | Version: "0:0.1.1-1.module+el8.1.0+7785+0ea9f177", 320 | }, 321 | }, 322 | majorVer: "8", 323 | }, 324 | expected: []models.Package{ 325 | { 326 | Name: "name-el8", 327 | Version: "0:0.0.1-0.0.1.el8", 328 | }, 329 | { 330 | Name: "name-module+el8", 331 | Version: "0:0.1.1-1.module+el8.1.0+7785+0ea9f177", 332 | }, 333 | }, 334 | }, 335 | { 336 | in: args{ 337 | packs: []models.Package{ 338 | { 339 | Name: "name-el7", 340 | Version: "0:0.0.1-0.0.1.el7", 341 | }, 342 | { 343 | Name: "name-el8", 344 | Version: "0:0.0.1-0.0.1.el8", 345 | }, 346 | { 347 | Name: "name-module+el7", 348 | Version: "0:0.1.1-1.module+el7.1.0+7785+0ea9f177", 349 | }, 350 | { 351 | Name: "name-module+el8", 352 | Version: "0:0.1.1-1.module+el8.1.0+7785+0ea9f177", 353 | }, 354 | }, 355 | majorVer: "", 356 | }, 357 | expected: []models.Package{ 358 | { 359 | Name: "name-el7", 360 | Version: "0:0.0.1-0.0.1.el7", 361 | }, 362 | { 363 | Name: "name-el8", 364 | Version: "0:0.0.1-0.0.1.el8", 365 | }, 366 | { 367 | Name: "name-module+el7", 368 | Version: "0:0.1.1-1.module+el7.1.0+7785+0ea9f177", 369 | }, 370 | { 371 | Name: "name-module+el8", 372 | Version: "0:0.1.1-1.module+el8.1.0+7785+0ea9f177", 373 | }, 374 | }, 375 | }, 376 | { 377 | in: args{ 378 | packs: []models.Package{ 379 | { 380 | Name: "name-el7", 381 | Version: "0:0.0.1-0.0.1.el7", 382 | }, 383 | { 384 | Name: "name-el8", 385 | Version: "0:0.0.1-0.0.1.el8", 386 | }, 387 | { 388 | Name: "notfixedyet", 389 | NotFixedYet: true, 390 | }, 391 | }, 392 | majorVer: "8", 393 | }, 394 | expected: []models.Package{ 395 | { 396 | Name: "name-el8", 397 | Version: "0:0.0.1-0.0.1.el8", 398 | }, 399 | { 400 | Name: "notfixedyet", 401 | NotFixedYet: true, 402 | }, 403 | }, 404 | }, 405 | } 406 | 407 | for i, tt := range tests { 408 | if aout := filterByRedHatMajor(tt.in.packs, tt.in.majorVer); !reflect.DeepEqual(aout, tt.expected) { 409 | t.Errorf("[%d] filterByRedHatMajor expected: %#v\n actual: %#v\n", i, tt.expected, aout) 410 | } 411 | } 412 | } 413 | -------------------------------------------------------------------------------- /models/redhat/types.go: -------------------------------------------------------------------------------- 1 | package redhat 2 | 3 | import "encoding/xml" 4 | 5 | // Root : root object 6 | type Root struct { 7 | XMLName xml.Name `xml:"oval_definitions"` 8 | Generator Generator `xml:"generator"` 9 | Definitions Definitions `xml:"definitions"` 10 | Tests Tests `xml:"tests"` 11 | Objects Objects `xml:"objects"` 12 | States States `xml:"states"` 13 | } 14 | 15 | // Generator : >generator 16 | type Generator struct { 17 | XMLName xml.Name `xml:"generator"` 18 | ProductName string `xml:"product_name"` 19 | ProductVersion string `xml:"product_version"` 20 | SchemaVersion string `xml:"schema_version"` 21 | Timestamp string `xml:"timestamp"` 22 | } 23 | 24 | // Definitions : >definitions 25 | type Definitions struct { 26 | XMLName xml.Name `xml:"definitions"` 27 | Definitions []Definition `xml:"definition"` 28 | } 29 | 30 | // Definition : >definitions>definition 31 | type Definition struct { 32 | XMLName xml.Name `xml:"definition"` 33 | ID string `xml:"id,attr"` 34 | Class string `xml:"class,attr"` 35 | Title string `xml:"metadata>title"` 36 | Affecteds []Affected `xml:"metadata>affected"` 37 | References []Reference `xml:"metadata>reference"` 38 | Description string `xml:"metadata>description"` 39 | Advisory Advisory `xml:"metadata>advisory"` 40 | Criteria Criteria `xml:"criteria"` 41 | } 42 | 43 | // Criteria : >definitions>definition>criteria 44 | type Criteria struct { 45 | XMLName xml.Name `xml:"criteria"` 46 | Operator string `xml:"operator,attr"` 47 | Criterias []Criteria `xml:"criteria"` 48 | Criterions []Criterion `xml:"criterion"` 49 | } 50 | 51 | // Criterion : >definitions>definition>criteria>*>criterion 52 | type Criterion struct { 53 | XMLName xml.Name `xml:"criterion"` 54 | TestRef string `xml:"test_ref,attr"` 55 | Comment string `xml:"comment,attr"` 56 | } 57 | 58 | // Affected : >definitions>definition>metadata>affected 59 | type Affected struct { 60 | XMLName xml.Name `xml:"affected"` 61 | Family string `xml:"family,attr"` 62 | Platforms []string `xml:"platform"` 63 | } 64 | 65 | // Reference : >definitions>definition>metadata>reference 66 | type Reference struct { 67 | XMLName xml.Name `xml:"reference"` 68 | Source string `xml:"source,attr"` 69 | RefID string `xml:"ref_id,attr"` 70 | RefURL string `xml:"ref_url,attr"` 71 | } 72 | 73 | // Advisory : >definitions>definition>metadata>advisory 74 | // RedHat and Ubuntu OVAL 75 | type Advisory struct { 76 | XMLName xml.Name `xml:"advisory"` 77 | Severity string `xml:"severity"` 78 | Rights string `xml:"rights"` 79 | Cves []Cve `xml:"cve"` 80 | Bugzillas []Bugzilla `xml:"bugzilla"` 81 | AffectedCPEList []string `xml:"affected_cpe_list>cpe"` 82 | Affected AffectedPkgs `xml:"affected"` 83 | Issued struct { 84 | Date string `xml:"date,attr"` 85 | } `xml:"issued"` 86 | Updated struct { 87 | Date string `xml:"date,attr"` 88 | } `xml:"updated"` 89 | } 90 | 91 | // Cve : >definitions>definition>metadata>advisory>cve 92 | type Cve struct { 93 | XMLName xml.Name `xml:"cve"` 94 | CveID string `xml:",chardata"` 95 | Cvss2 string `xml:"cvss2,attr"` 96 | Cvss3 string `xml:"cvss3,attr"` 97 | Cwe string `xml:"cwe,attr"` 98 | Impact string `xml:"impact,attr"` 99 | Href string `xml:"href,attr"` 100 | Public string `xml:"public,attr"` 101 | } 102 | 103 | // Bugzilla : >definitions>definition>metadata>advisory>bugzilla 104 | type Bugzilla struct { 105 | XMLName xml.Name `xml:"bugzilla"` 106 | ID string `xml:"id,attr"` 107 | URL string `xml:"href,attr"` 108 | Title string `xml:",chardata"` 109 | } 110 | 111 | // AffectedPkgs : >definitions>definition>metadata>advisory>affected 112 | type AffectedPkgs struct { 113 | Resolution []struct { 114 | State string `xml:"state,attr"` 115 | Component []string `xml:"component"` 116 | } `xml:"resolution"` 117 | } 118 | 119 | // Tests : >tests 120 | type Tests struct { 121 | XMLName xml.Name `xml:"tests"` 122 | RpminfoTests []RpminfoTest `xml:"rpminfo_test"` 123 | RpmverifyfileTests []RpmverifyfileTest `xml:"rpmverifyfile_test"` 124 | Textfilecontent54Tests []Textfilecontent54Test `xml:"textfilecontent54_test"` 125 | UnameTests []UnameTest `xml:"uname_test"` 126 | } 127 | 128 | // RpminfoTest : >tests>rpminfo_test 129 | type RpminfoTest struct { 130 | Check string `xml:"check,attr"` 131 | Comment string `xml:"comment,attr"` 132 | ID string `xml:"id,attr"` 133 | Version string `xml:"version,attr"` 134 | CheckExistence string `xml:"check_existence,attr"` 135 | Object ObjectRef `xml:"object"` 136 | State StateRef `xml:"state"` 137 | } 138 | 139 | // RpmverifyfileTest : tests>rpmverifyfile_test 140 | type RpmverifyfileTest struct { 141 | Check string `xml:"check,attr"` 142 | Comment string `xml:"comment,attr"` 143 | ID string `xml:"id,attr"` 144 | Version string `xml:"version,attr"` 145 | Object ObjectRef `xml:"object"` 146 | State StateRef `xml:"state"` 147 | } 148 | 149 | // Textfilecontent54Test : tests>textfilecontent54_test 150 | type Textfilecontent54Test struct { 151 | Check string `xml:"check,attr"` 152 | Comment string `xml:"comment,attr"` 153 | ID string `xml:"id,attr"` 154 | Version string `xml:"version,attr"` 155 | Object ObjectRef `xml:"object"` 156 | State StateRef `xml:"state"` 157 | } 158 | 159 | // UnameTest : tests>uname_test 160 | type UnameTest struct { 161 | Check string `xml:"check,attr"` 162 | Comment string `xml:"comment,attr"` 163 | ID string `xml:"id,attr"` 164 | Version string `xml:"version,attr"` 165 | Object ObjectRef `xml:"object"` 166 | State StateRef `xml:"state"` 167 | } 168 | 169 | // ObjectRef : 170 | // >tests>rpminfo_test>object-object_ref 171 | // >tests>rpmverifyfile_test>object-object_ref 172 | // >tests>textfilecontent54_test>object-object_ref 173 | // >tests>uname_test>object-object_ref 174 | type ObjectRef struct { 175 | XMLName xml.Name `xml:"object"` 176 | Text string `xml:",chardata"` 177 | ObjectRef string `xml:"object_ref,attr"` 178 | } 179 | 180 | // StateRef : 181 | // >tests>rpminfo_test>state-state_ref 182 | // >tests>rpmverifyfile_test>state-state_ref 183 | // >tests>textfilecontent54_test>state-state_ref 184 | // >tests>uname_test>state-state_ref 185 | type StateRef struct { 186 | XMLName xml.Name `xml:"state"` 187 | Text string `xml:",chardata"` 188 | StateRef string `xml:"state_ref,attr"` 189 | } 190 | 191 | // Objects : >objects 192 | type Objects struct { 193 | XMLName xml.Name `xml:"objects"` 194 | RpminfoObjects []RpminfoObject `xml:"rpminfo_object"` 195 | RpmverifyfileObjects []RpmverifyfileObject `xml:"rpmverifyfile_object"` 196 | Textfilecontent54Objects []Textfilecontent54Object `xml:"textfilecontent54_object"` 197 | UnameObjects UnameObject `xml:"uname_object"` 198 | } 199 | 200 | // RpminfoObject : >objects>rpminfo_object 201 | type RpminfoObject struct { 202 | ID string `xml:"id,attr"` 203 | Version string `xml:"version,attr"` 204 | Name string `xml:"name"` 205 | } 206 | 207 | // RpmverifyfileObject : >objects>rpmverifyfile_object 208 | type RpmverifyfileObject struct { 209 | ID string `xml:"id,attr"` 210 | AttrVersion string `xml:"version,attr"` 211 | Behaviors struct { 212 | Text string `xml:",chardata"` 213 | Noconfigfiles string `xml:"noconfigfiles,attr"` 214 | Noghostfiles string `xml:"noghostfiles,attr"` 215 | Nogroup string `xml:"nogroup,attr"` 216 | Nolinkto string `xml:"nolinkto,attr"` 217 | Nomd5 string `xml:"nomd5,attr"` 218 | Nomode string `xml:"nomode,attr"` 219 | Nomtime string `xml:"nomtime,attr"` 220 | Nordev string `xml:"nordev,attr"` 221 | Nosize string `xml:"nosize,attr"` 222 | Nouser string `xml:"nouser,attr"` 223 | } `xml:"behaviors"` 224 | Name struct { 225 | Text string `xml:",chardata"` 226 | Operation string `xml:"operation,attr"` 227 | } `xml:"name"` 228 | Epoch struct { 229 | Text string `xml:",chardata"` 230 | Operation string `xml:"operation,attr"` 231 | } `xml:"epoch"` 232 | Version struct { 233 | Text string `xml:",chardata"` 234 | Operation string `xml:"operation,attr"` 235 | } `xml:"version"` 236 | Release struct { 237 | Text string `xml:",chardata"` 238 | Operation string `xml:"operation,attr"` 239 | } `xml:"release"` 240 | Arch struct { 241 | Text string `xml:",chardata"` 242 | Operation string `xml:"operation,attr"` 243 | } `xml:"arch"` 244 | Filepath string `xml:"filepath"` 245 | } 246 | 247 | // Textfilecontent54Object : >objects>textfilecontent54_object 248 | type Textfilecontent54Object struct { 249 | ID string `xml:"id,attr"` 250 | Version string `xml:"version,attr"` 251 | Filepath struct { 252 | Text string `xml:",chardata"` 253 | Datatype string `xml:"datatype,attr"` 254 | } `xml:"filepath"` 255 | Pattern struct { 256 | Text string `xml:",chardata"` 257 | Operation string `xml:"operation,attr"` 258 | } `xml:"pattern"` 259 | Instance struct { 260 | Text string `xml:",chardata"` 261 | Datatype string `xml:"datatype,attr"` 262 | VarRef string `xml:"var_ref,attr"` 263 | } `xml:"instance"` 264 | } 265 | 266 | // UnameObject : >objects>uname_object 267 | type UnameObject struct { 268 | ID string `xml:"id,attr"` 269 | Version string `xml:"version,attr"` 270 | } 271 | 272 | // States : >states 273 | type States struct { 274 | XMLName xml.Name `xml:"states"` 275 | RpminfoStates []RpminfoState `xml:"rpminfo_state"` 276 | RpmverifyfileStates []RpmverifyfileState `xml:"rpmverifyfile_state"` 277 | Textfilecontent54States []Textfilecontent54State `xml:"textfilecontent54_state"` 278 | UnameStates []UnameState `xml:"uname_state"` 279 | } 280 | 281 | // RpminfoState : >states>rpminfo_state 282 | type RpminfoState struct { 283 | ID string `xml:"id,attr"` 284 | Version string `xml:"version,attr"` 285 | Evr struct { 286 | Text string `xml:",chardata"` 287 | Datatype string `xml:"datatype,attr"` 288 | Operation string `xml:"operation,attr"` 289 | } `xml:"evr"` 290 | SignatureKeyid SignatureKeyid `xml:"signature_keyid"` 291 | Arch struct { 292 | Text string `xml:",chardata"` 293 | Datatype string `xml:"datatype,attr"` 294 | Operation string `xml:"operation,attr"` 295 | } `xml:"arch"` 296 | } 297 | 298 | // SignatureKeyid : >states>rpminfo_state>signature_keyid 299 | type SignatureKeyid struct { 300 | Text string `xml:",chardata"` 301 | Operation string `xml:"operation,attr"` 302 | } 303 | 304 | // RpmverifyfileState : >states>rpmverifyfile_state 305 | type RpmverifyfileState struct { 306 | ID string `xml:"id,attr"` 307 | AttrVersion string `xml:"version,attr"` 308 | Name struct { 309 | Text string `xml:",chardata"` 310 | Operation string `xml:"operation,attr"` 311 | } `xml:"name"` 312 | Version struct { 313 | Text string `xml:",chardata"` 314 | Operation string `xml:"operation,attr"` 315 | } `xml:"version"` 316 | } 317 | 318 | // Textfilecontent54State : >states>textfilecontent54_state 319 | type Textfilecontent54State struct { 320 | ID string `xml:"id,attr"` 321 | Version string `xml:"version,attr"` 322 | Text struct { 323 | Text string `xml:",chardata"` 324 | Operation string `xml:"operation,attr"` 325 | } `xml:"text"` 326 | } 327 | 328 | // UnameState : >states>uname_state 329 | type UnameState struct { 330 | ID string `xml:"id,attr"` 331 | Version string `xml:"version,attr"` 332 | OsRelease struct { 333 | Text string `xml:",chardata"` 334 | Operation string `xml:"operation,attr"` 335 | } `xml:"os_release"` 336 | } 337 | --------------------------------------------------------------------------------