├── doc └── gost01.gif ├── main.go ├── .github ├── ISSUE_TEMPLATE │ ├── FEATURE_REQUEST.md │ ├── SUPPORT_QUESTION.md │ └── BUG_REPORT.md ├── workflows │ ├── test.yml │ ├── goreleaser.yml │ ├── golangci.yml │ ├── docker-build.yml │ └── fetch.yml ├── dependabot.yml └── PULL_REQUEST_TEMPLATE.md ├── cmd ├── version.go ├── fetch.go ├── server.go ├── arch.go ├── debian.go ├── microsoft.go ├── ubuntu.go ├── redhat.go ├── root.go ├── redhatapi.go ├── notify.go └── register.go ├── .gitignore ├── models ├── models.go ├── models_test.go ├── arch.go ├── debian.go ├── ubuntu.go ├── microsoft.go └── redhat.go ├── Dockerfile ├── .goreleaser.yml ├── .revive.toml ├── fetcher ├── arch.go ├── ubuntu.go ├── microsoft.go ├── util.go ├── redhatapi.go ├── redhat.go └── debian.go ├── LICENSE ├── GNUmakefile ├── config └── config.go ├── notifier ├── slack.go ├── email.go └── redhat.go ├── .golangci.yml ├── db ├── db.go ├── arch.go ├── debian.go ├── rdb.go ├── redhat.go ├── ubuntu.go └── microsoft.go ├── go.mod ├── util └── util.go ├── README.md └── server └── server.go /doc/gost01.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vulsio/gost/HEAD/doc/gost01.gif -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/vulsio/gost/cmd" 8 | ) 9 | 10 | func main() { 11 | if err := cmd.RootCmd.Execute(); err != nil { 12 | fmt.Fprintln(os.Stderr, err) 13 | os.Exit(1) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | 8 | "github.com/vulsio/gost/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("gost %s %s\n", config.Version, config.Revision) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | 7 | # Test binary, build with `go test -c` 8 | *.test 9 | 10 | # Output of the go coverage tool, specifically when used with LiteIDE 11 | *.out 12 | 13 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 14 | .glide/ 15 | 16 | vendor/ 17 | 18 | *.sqlite3 19 | *.sqlite3-shm 20 | *.sqlite3-wal 21 | *.sqlite3-journal 22 | 23 | *.toml 24 | !.revive.toml 25 | 26 | .vscode 27 | 28 | # binary 29 | gost 30 | -------------------------------------------------------------------------------- /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 Gost. 10 | const LatestSchemaVersion = 3 11 | 12 | // FetchMeta has meta information about fetched security tracker 13 | type FetchMeta struct { 14 | gorm.Model `json:"-"` 15 | GostRevision 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/gost 10 | COPY . $GOPATH/src/$REPOSITORY 11 | RUN cd $GOPATH/src/$REPOSITORY && make install 12 | 13 | 14 | FROM alpine:3.22 15 | 16 | ENV LOGDIR /var/log/gost 17 | ENV WORKDIR /gost 18 | 19 | RUN apk add --no-cache ca-certificates git \ 20 | && mkdir -p $WORKDIR $LOGDIR 21 | 22 | COPY --from=builder /go/bin/gost /usr/local/bin/ 23 | 24 | VOLUME ["$WORKDIR", "$LOGDIR"] 25 | WORKDIR $WORKDIR 26 | ENV PWD $WORKDIR 27 | 28 | ENTRYPOINT ["gost"] 29 | CMD ["--help"] 30 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | project_name: gost 2 | release: 3 | github: 4 | owner: vulsio 5 | name: gost 6 | env: 7 | - CGO_ENABLED=0 8 | builds: 9 | - id: gost 10 | goos: 11 | - linux 12 | - windows 13 | - darwin 14 | goarch: 15 | - amd64 16 | - arm64 17 | main: . 18 | ldflags: -s -w -X github.com/vulsio/gost/config.Version={{.Version}} -X github.com/vulsio/gost/config.Revision={{.Commit}} 19 | binary: gost 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 | -------------------------------------------------------------------------------- /.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] -------------------------------------------------------------------------------- /.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 | 33 | -------------------------------------------------------------------------------- /.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 | * gost environment: 29 | 30 | Hash : ____ 31 | 32 | To check the commit hash of HEAD 33 | $ gost version 34 | 35 | or 36 | 37 | $ cd $GOPATH/src/github.com/vulsio/gost 38 | $ git rev-parse --short HEAD 39 | 40 | * command: 41 | 42 | -------------------------------------------------------------------------------- /cmd/fetch.go: -------------------------------------------------------------------------------- 1 | package cmd 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 the data of the security tracker", 12 | Long: `Fetch the data of the security tracker`, 13 | } 14 | 15 | func init() { 16 | RootCmd.AddCommand(fetchCmd) 17 | 18 | fetchCmd.PersistentFlags().Int("wait", 0, "Interval between fetch (seconds)") 19 | _ = viper.BindPFlag("wait", fetchCmd.PersistentFlags().Lookup("wait")) 20 | 21 | fetchCmd.PersistentFlags().Int("threads", 5, "The number of threads to be used") 22 | _ = viper.BindPFlag("threads", fetchCmd.PersistentFlags().Lookup("threads")) 23 | 24 | fetchCmd.PersistentFlags().Int("batch-size", 1, "The number of batch size to insert.") 25 | _ = viper.BindPFlag("batch-size", fetchCmd.PersistentFlags().Lookup("batch-size")) 26 | } 27 | -------------------------------------------------------------------------------- /fetcher/arch.go: -------------------------------------------------------------------------------- 1 | package fetcher 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | 7 | "github.com/vulsio/gost/models" 8 | "github.com/vulsio/gost/util" 9 | "golang.org/x/xerrors" 10 | ) 11 | 12 | const archAdvURL = "https://security.archlinux.org/json" 13 | 14 | // FetchArch fetch Advisory JSONs 15 | func FetchArch() ([]models.ArchADVJSON, error) { 16 | resp, err := util.FetchURL(archAdvURL) 17 | if err != nil { 18 | return nil, xerrors.Errorf("Failed to fetch Security Advisory from Arch Linux. err: %w", err) 19 | } 20 | defer resp.Body.Close() 21 | 22 | if resp.StatusCode != http.StatusOK { 23 | return nil, xerrors.Errorf("Failed to fetch Security Advisory from Arch Linux. err: status code: %d", resp.StatusCode) 24 | } 25 | 26 | var advs []models.ArchADVJSON 27 | if err := json.NewDecoder(resp.Body).Decode(&advs); err != nil { 28 | return nil, xerrors.Errorf("Failed to unmarshal Arch Linux Security Advisory JSON. err: %w", err) 29 | } 30 | 31 | return advs, nil 32 | } 33 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © 2017 Teppei Fukuda 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /.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/gost 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 | tags: | 43 | vuls/gost:latest 44 | ${{ steps.meta.outputs.tags }} 45 | secrets: | 46 | "github_token=${{ secrets.GITHUB_TOKEN }}" 47 | platforms: linux/amd64,linux/arm64 48 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /GNUmakefile: -------------------------------------------------------------------------------- 1 | .PHONY: \ 2 | all \ 3 | build \ 4 | install \ 5 | lint \ 6 | golangci \ 7 | vet \ 8 | fmt \ 9 | fmtcheck \ 10 | pretest \ 11 | test \ 12 | integration \ 13 | cov \ 14 | clean 15 | 16 | SRCS = $(shell git ls-files '*.go') 17 | PKGS = $(shell go list ./...) 18 | VERSION := $(shell git describe --tags --abbrev=0) 19 | REVISION := $(shell git rev-parse --short HEAD) 20 | LDFLAGS := -X 'github.com/vulsio/gost/config.Version=$(VERSION)' \ 21 | -X 'github.com/vulsio/gost/config.Revision=$(REVISION)' 22 | GO := CGO_ENABLED=0 go 23 | 24 | all: build test 25 | 26 | build: main.go 27 | $(GO) build -ldflags "$(LDFLAGS)" -o gost $< 28 | 29 | install: main.go 30 | $(GO) install -ldflags "$(LDFLAGS)" 31 | 32 | lint: 33 | go install github.com/mgechev/revive@latest 34 | revive -config ./.revive.toml -formatter plain $(PKGS) 35 | 36 | golangci: 37 | go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest 38 | golangci-lint run 39 | 40 | vet: 41 | echo $(PKGS) | xargs env $(GO) vet || exit; 42 | 43 | fmt: 44 | gofmt -s -w $(SRCS) 45 | 46 | fmtcheck: 47 | $(foreach file,$(SRCS),gofmt -s -d $(file);) 48 | 49 | pretest: lint vet fmtcheck 50 | 51 | test: pretest 52 | $(GO) test -cover -v ./... || exit; 53 | 54 | cov: 55 | @ go get -v github.com/axw/gocov/gocov 56 | @ go get golang.org/x/tools/cmd/cover 57 | gocov test | gocov report 58 | 59 | clean: 60 | $(foreach pkg,$(PKGS),go clean $(pkg) || exit;) 61 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | // Version of Gost 4 | var Version = "`make build` or `make install` will show the version" 5 | 6 | // Revision of Git 7 | var Revision string 8 | 9 | // Config for register and notify command 10 | type Config struct { 11 | Redhat map[string]RedhatWatchCve `toml:"redhat"` 12 | EMail SMTPConf 13 | Slack SlackConf 14 | } 15 | 16 | // RedhatWatchCve for watch redhat cve 17 | type RedhatWatchCve struct { 18 | ThreatSeverity bool `toml:"threat_severity"` 19 | Bugzilla bool `toml:"bugzilla"` 20 | Cvss bool `toml:"cvss"` 21 | Cvss3 bool `toml:"cvss3"` 22 | Statement bool `toml:"statement"` 23 | Acknowledgement bool `toml:"acknowledgement"` 24 | Mitigation bool `toml:"mitigation"` 25 | AffectedRelease bool `toml:"affected_release"` 26 | PackageState bool `toml:"package_state"` 27 | Reference bool `toml:"reference"` 28 | Details bool `toml:"details"` 29 | } 30 | 31 | // SMTPConf is smtp config 32 | type SMTPConf struct { 33 | SMTPAddr string 34 | SMTPPort string `valid:"port"` 35 | 36 | User string 37 | Password string 38 | From string 39 | To []string 40 | Cc []string 41 | SubjectPrefix string 42 | 43 | UseThisTime bool 44 | } 45 | 46 | // SlackConf is slack config 47 | type SlackConf struct { 48 | HookURL string `valid:"url"` 49 | Channel string `json:"channel"` 50 | IconEmoji string `json:"icon_emoji"` 51 | AuthUser string `json:"username"` 52 | 53 | NotifyUsers []string 54 | Text string `json:"text"` 55 | 56 | UseThisTime bool 57 | } 58 | -------------------------------------------------------------------------------- /notifier/slack.go: -------------------------------------------------------------------------------- 1 | package notifier 2 | 3 | import ( 4 | "encoding/json" 5 | "time" 6 | 7 | "github.com/cenkalti/backoff" 8 | "github.com/inconshreveable/log15" 9 | "github.com/parnurzeal/gorequest" 10 | "github.com/spf13/viper" 11 | "golang.org/x/xerrors" 12 | 13 | "github.com/vulsio/gost/config" 14 | ) 15 | 16 | type message struct { 17 | Text string `json:"text"` 18 | Username string `json:"username"` 19 | IconEmoji string `json:"icon_emoji"` 20 | Channel string `json:"channel"` 21 | } 22 | 23 | // SendSlack sends a message to Slack 24 | func SendSlack(txt string, conf config.SlackConf) error { 25 | msg := message{ 26 | Text: "```" + txt + "```", 27 | Username: conf.AuthUser, 28 | IconEmoji: conf.IconEmoji, 29 | Channel: conf.Channel, 30 | } 31 | return send(msg, conf) 32 | } 33 | 34 | func send(msg message, conf config.SlackConf) error { 35 | count, retryMax := 0, 10 36 | 37 | bytes, _ := json.Marshal(msg) 38 | jsonBody := string(bytes) 39 | 40 | f := func() (err error) { 41 | resp, body, errs := gorequest.New().Proxy(viper.GetString("http-proxy")).Post(conf.HookURL).Send(string(jsonBody)).End() 42 | if 0 < len(errs) || resp == nil || resp.StatusCode != 200 { 43 | count++ 44 | if count == retryMax { 45 | return nil 46 | } 47 | return xerrors.Errorf("HTTP POST error: %v, url: %s, resp: %v, body: %s", errs, conf.HookURL, resp, body) 48 | } 49 | return nil 50 | } 51 | notify := func(err error, t time.Duration) { 52 | log15.Warn("Error", "err", err) 53 | log15.Warn("Retrying", "in", t) 54 | } 55 | boff := backoff.NewExponentialBackOff() 56 | if err := backoff.RetryNotify(f, boff, notify); err != nil { 57 | return xerrors.Errorf("HTTP error: %w", err) 58 | } 59 | if count == retryMax { 60 | return xerrors.Errorf("Retry count exceeded") 61 | } 62 | return nil 63 | } 64 | -------------------------------------------------------------------------------- /.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 | revive: # https://golangci-lint.run/usage/linters/#revive 15 | rules: 16 | - name: blank-imports 17 | - name: context-as-argument 18 | - name: context-keys-type 19 | - name: dot-imports 20 | - name: empty-block 21 | - name: error-naming 22 | - name: error-return 23 | - name: error-strings 24 | - name: errorf 25 | - name: exported 26 | - name: if-return 27 | - name: increment-decrement 28 | - name: indent-error-flow 29 | - name: package-comments 30 | disabled: true 31 | - name: range 32 | - name: receiver-naming 33 | - name: redefines-builtin-id 34 | - name: superfluous-else 35 | - name: time-naming 36 | - name: unexported-return 37 | - name: unreachable-code 38 | - name: unused-parameter 39 | - name: var-declaration 40 | - name: var-naming 41 | arguments: 42 | - [] # AllowList 43 | - [] # DenyList 44 | - - skip-package-name-checks: true 45 | staticcheck: # https://golangci-lint.run/usage/linters/#staticcheck 46 | checks: 47 | - all 48 | - -ST1000 # at least one file in a package should have a package comment 49 | - -ST1005 # error strings should not be capitalized 50 | exclusions: 51 | rules: 52 | - source: "defer .+\\.Close\\(\\)" 53 | linters: 54 | - errcheck 55 | - source: "defer os.RemoveAll\\(.+\\)" 56 | linters: 57 | - errcheck 58 | 59 | formatters: 60 | enable: 61 | - goimports 62 | 63 | run: 64 | timeout: 10m 65 | -------------------------------------------------------------------------------- /fetcher/ubuntu.go: -------------------------------------------------------------------------------- 1 | package fetcher 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "io/fs" 8 | "iter" 9 | "os" 10 | "path/filepath" 11 | 12 | "golang.org/x/xerrors" 13 | 14 | "github.com/vulsio/gost/models" 15 | "github.com/vulsio/gost/util" 16 | ) 17 | 18 | const ( 19 | ubuntuRepoURL = "https://github.com/aquasecurity/vuln-list/archive/refs/heads/main.tar.gz" 20 | ubuntuDir = "ubuntu" 21 | ) 22 | 23 | // FetchUbuntuVulnList clones vuln-list and returns CVE JSONs 24 | func FetchUbuntuVulnList() (iter.Seq2[models.UbuntuCVEJSON, error], error) { 25 | if err := fetchGitArchive(ubuntuRepoURL, filepath.Join(util.CacheDir(), "vuln-list"), fmt.Sprintf("vuln-list-main/%s", ubuntuDir)); err != nil { 26 | return nil, xerrors.Errorf("Failed to fetch vuln-list-ubuntu: %w", err) 27 | } 28 | 29 | return func(yield func(models.UbuntuCVEJSON, error) bool) { 30 | var yieldErr = errors.New("yield error") 31 | if err := filepath.WalkDir(filepath.Join(util.CacheDir(), "vuln-list"), func(path string, d fs.DirEntry, err error) error { 32 | if err != nil { 33 | return err 34 | } 35 | 36 | if d.IsDir() { 37 | return nil 38 | } 39 | 40 | f, err := os.Open(path) 41 | if err != nil { 42 | return xerrors.Errorf("Failed to open file: %w", err) 43 | } 44 | defer f.Close() 45 | 46 | cve := models.UbuntuCVEJSON{} 47 | if err = json.NewDecoder(f).Decode(&cve); err != nil { 48 | return xerrors.Errorf("failed to decode Ubuntu JSON: %w", err) 49 | } 50 | 51 | if !yield(cve, nil) { 52 | return yieldErr 53 | } 54 | 55 | return nil 56 | }); err != nil { 57 | if errors.Is(err, yieldErr) { // No need to call yield with error 58 | return 59 | } 60 | if !yield(models.UbuntuCVEJSON{}, xerrors.Errorf("Failed to walk %s: %w", filepath.Join(util.CacheDir(), "vuln-list"), err)) { 61 | return 62 | } 63 | } 64 | }, nil 65 | } 66 | -------------------------------------------------------------------------------- /fetcher/microsoft.go: -------------------------------------------------------------------------------- 1 | package fetcher 2 | 3 | import ( 4 | "compress/gzip" 5 | "encoding/json" 6 | "net/http" 7 | 8 | "github.com/vulsio/gost/models" 9 | "github.com/vulsio/gost/util" 10 | "golang.org/x/xerrors" 11 | ) 12 | 13 | const ( 14 | vulnerabilityURL = "https://raw.githubusercontent.com/vulsio/windows-vuln-feed/main/dist/vulnerability/vulnerability.json.gz" 15 | supercedenceURL = "https://raw.githubusercontent.com/vulsio/windows-vuln-feed/main/dist/supercedence/supercedence.json.gz" 16 | ) 17 | 18 | // RetrieveMicrosoftCveDetails : 19 | func RetrieveMicrosoftCveDetails() ([]models.MicrosoftVulnerability, []models.MicrosoftSupercedence, error) { 20 | resp, err := util.FetchURL(vulnerabilityURL) 21 | if err != nil { 22 | return nil, nil, xerrors.Errorf("Failed to fetch Microsoft Vulnerability data. err: %w", err) 23 | } 24 | defer resp.Body.Close() 25 | 26 | if resp.StatusCode != http.StatusOK { 27 | return nil, nil, xerrors.Errorf("Failed to fetch Microsoft Vulnerability data. err: status code: %d", resp.StatusCode) 28 | } 29 | 30 | gz, err := gzip.NewReader(resp.Body) 31 | if err != nil { 32 | return nil, nil, err 33 | } 34 | defer gz.Close() 35 | var vulns []models.MicrosoftVulnerability 36 | if err := json.NewDecoder(gz).Decode(&vulns); err != nil { 37 | return nil, nil, err 38 | } 39 | 40 | resp, err = util.FetchURL(supercedenceURL) 41 | if err != nil { 42 | return nil, nil, err 43 | } 44 | defer resp.Body.Close() 45 | 46 | if resp.StatusCode != http.StatusOK { 47 | return nil, nil, xerrors.Errorf("Failed to fetch Microsoft Supercedence data. err: status code: %d", resp.StatusCode) 48 | } 49 | 50 | gz, err = gzip.NewReader(resp.Body) 51 | if err != nil { 52 | return nil, nil, err 53 | } 54 | var supercedences []models.MicrosoftSupercedence 55 | if err := json.NewDecoder(gz).Decode(&supercedences); err != nil { 56 | return nil, nil, err 57 | } 58 | 59 | return vulns, supercedences, nil 60 | } 61 | -------------------------------------------------------------------------------- /notifier/email.go: -------------------------------------------------------------------------------- 1 | package notifier 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "net/mail" 7 | "net/smtp" 8 | "strings" 9 | "time" 10 | 11 | "golang.org/x/xerrors" 12 | 13 | "github.com/vulsio/gost/config" 14 | ) 15 | 16 | // EMailSender is interface of sending e-mail 17 | type EMailSender interface { 18 | Send(subject, body string) error 19 | } 20 | 21 | type emailSender struct { 22 | conf config.SMTPConf 23 | send func(string, smtp.Auth, string, []string, []byte) error 24 | } 25 | 26 | // NewEMailSender creates emailSender 27 | func NewEMailSender(config config.SMTPConf) EMailSender { 28 | return &emailSender{config, smtp.SendMail} 29 | } 30 | 31 | func (e *emailSender) Send(subject, body string) (err error) { 32 | emailConf := e.conf 33 | to := strings.Join(emailConf.To[:], ", ") 34 | cc := strings.Join(emailConf.Cc[:], ", ") 35 | mailAddresses := append(emailConf.To, emailConf.Cc...) 36 | if _, err := mail.ParseAddressList(strings.Join(mailAddresses[:], ", ")); err != nil { 37 | return xerrors.Errorf("Failed to parse email addresses: %w", err) 38 | } 39 | 40 | headers := make(map[string]string) 41 | headers["From"] = emailConf.From 42 | headers["To"] = to 43 | headers["Cc"] = cc 44 | headers["Subject"] = subject 45 | headers["Date"] = time.Now().Format(time.RFC1123Z) 46 | headers["Content-Type"] = "text/plain; charset=utf-8" 47 | 48 | var header string 49 | for k, v := range headers { 50 | header += fmt.Sprintf("%s: %s\r\n", k, v) 51 | } 52 | message := fmt.Sprintf("%s\r\n%s", header, body) 53 | 54 | smtpServer := net.JoinHostPort(emailConf.SMTPAddr, emailConf.SMTPPort) 55 | err = e.send( 56 | smtpServer, 57 | smtp.PlainAuth( 58 | "", 59 | emailConf.User, 60 | emailConf.Password, 61 | emailConf.SMTPAddr, 62 | ), 63 | emailConf.From, 64 | mailAddresses, 65 | []byte(message), 66 | ) 67 | if err != nil { 68 | return xerrors.Errorf("Failed to send emails: %w", err) 69 | } 70 | return nil 71 | } 72 | -------------------------------------------------------------------------------- /fetcher/util.go: -------------------------------------------------------------------------------- 1 | package fetcher 2 | 3 | import ( 4 | "archive/tar" 5 | "compress/gzip" 6 | "io" 7 | "net/http" 8 | "os" 9 | "path/filepath" 10 | "strings" 11 | 12 | "golang.org/x/xerrors" 13 | 14 | "github.com/vulsio/gost/util" 15 | ) 16 | 17 | func fetchGitArchive(url, fetchDir, targetDirPrefix string) error { 18 | if err := os.RemoveAll(fetchDir); err != nil { 19 | return xerrors.Errorf("Failed to remove directory. err: %w", err) 20 | } 21 | 22 | resp, err := util.FetchURL(url) 23 | if err != nil { 24 | return xerrors.Errorf("Failed to fetch git archive. err: %w", err) 25 | } 26 | defer resp.Body.Close() 27 | 28 | if resp.StatusCode != http.StatusOK { 29 | return xerrors.Errorf("Failed to fetch git archive. err: status code: %d", resp.StatusCode) 30 | } 31 | 32 | gr, err := gzip.NewReader(resp.Body) 33 | if err != nil { 34 | return xerrors.Errorf("Failed to create gzip reader. err: %w", err) 35 | } 36 | defer gr.Close() 37 | 38 | tr := tar.NewReader(gr) 39 | for { 40 | hdr, err := tr.Next() 41 | if err == io.EOF { 42 | break 43 | } 44 | if err != nil { 45 | return xerrors.Errorf("Failed to read tar header. err: %w", err) 46 | } 47 | 48 | switch hdr.Typeflag { 49 | case tar.TypeReg: 50 | if !strings.HasPrefix(hdr.Name, targetDirPrefix) { 51 | break 52 | } 53 | 54 | filePath := filepath.Join(fetchDir, hdr.Name) 55 | 56 | if err := os.MkdirAll(filepath.Dir(filePath), 0755); err != nil { 57 | return xerrors.Errorf("Failed to create directory. err: %w", err) 58 | } 59 | 60 | if err := func() error { 61 | f, err := os.Create(filePath) 62 | if err != nil { 63 | return xerrors.Errorf("Failed to create file. err: %w", err) 64 | } 65 | defer f.Close() 66 | 67 | if _, err := io.Copy(f, tr); err != nil { 68 | return xerrors.Errorf("Failed to write file. err: %w", err) 69 | } 70 | 71 | return nil 72 | }(); err != nil { 73 | return xerrors.Errorf("Failed to create file. err: %w", err) 74 | } 75 | default: 76 | } 77 | } 78 | 79 | return nil 80 | } 81 | -------------------------------------------------------------------------------- /cmd/server.go: -------------------------------------------------------------------------------- 1 | package cmd 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/gost/db" 12 | "github.com/vulsio/gost/models" 13 | "github.com/vulsio/gost/server" 14 | "github.com/vulsio/gost/util" 15 | ) 16 | 17 | // serverCmd represents the server command 18 | var serverCmd = &cobra.Command{ 19 | Use: "server", 20 | Short: "Start security tracker HTTP server", 21 | Long: `Start security tracker 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", "1325", "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 := util.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 | -------------------------------------------------------------------------------- /cmd/arch.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | 7 | "github.com/inconshreveable/log15" 8 | "github.com/spf13/cobra" 9 | "github.com/spf13/viper" 10 | "github.com/vulsio/gost/db" 11 | "github.com/vulsio/gost/fetcher" 12 | "github.com/vulsio/gost/models" 13 | "github.com/vulsio/gost/util" 14 | "golang.org/x/xerrors" 15 | ) 16 | 17 | var archCmd = &cobra.Command{ 18 | Use: "arch", 19 | Short: "Fetch the CVE information from Arch Linux", 20 | Long: `Fetch the CVE information from Arch Linux`, 21 | RunE: fetchArch, 22 | } 23 | 24 | func init() { 25 | fetchCmd.AddCommand(archCmd) 26 | } 27 | 28 | func fetchArch(_ *cobra.Command, _ []string) (err error) { 29 | if err := util.SetLogger(viper.GetBool("log-to-file"), viper.GetString("log-dir"), viper.GetBool("debug"), viper.GetBool("log-json")); err != nil { 30 | return xerrors.Errorf("Failed to SetLogger. err: %w", err) 31 | } 32 | 33 | log15.Info("Initialize Database") 34 | driver, err := db.NewDB(viper.GetString("dbtype"), viper.GetString("dbpath"), viper.GetBool("debug-sql"), db.Option{}) 35 | if err != nil { 36 | if errors.Is(err, db.ErrDBLocked) { 37 | return xerrors.Errorf("Failed to open DB. Close DB connection before fetching. err: %w", err) 38 | } 39 | return xerrors.Errorf("Failed to open DB. err: %w", err) 40 | } 41 | 42 | fetchMeta, err := driver.GetFetchMeta() 43 | if err != nil { 44 | return xerrors.Errorf("Failed to get FetchMeta from DB. err: %w", err) 45 | } 46 | if fetchMeta.OutDated() { 47 | return xerrors.Errorf("Failed to Insert CVEs into DB. err: SchemaVersion is old. SchemaVersion: %+v", map[string]uint{"latest": models.LatestSchemaVersion, "DB": fetchMeta.SchemaVersion}) 48 | } 49 | // If the fetch fails the first time (without SchemaVersion), the DB needs to be cleaned every time, so insert SchemaVersion. 50 | if err := driver.UpsertFetchMeta(fetchMeta); err != nil { 51 | return xerrors.Errorf("Failed to upsert FetchMeta to DB. err: %w", err) 52 | } 53 | 54 | log15.Info("Fetched all CVEs from Arch Linux") 55 | advJSONs, err := fetcher.FetchArch() 56 | if err != nil { 57 | return xerrors.Errorf("Failed to fetch Arch. err: %w", err) 58 | } 59 | advs := models.ConvertArch(advJSONs) 60 | 61 | log15.Info("Fetched", "Advisories", len(advs)) 62 | 63 | log15.Info("Insert Arch Linux CVEs into DB", "db", driver.Name()) 64 | if err := driver.InsertArch(advs); err != nil { 65 | return xerrors.Errorf("Failed to insert. dbpath: %s, err: %w", viper.GetString("dbpath"), err) 66 | } 67 | 68 | fetchMeta.LastFetchedAt = time.Now() 69 | if err := driver.UpsertFetchMeta(fetchMeta); err != nil { 70 | return xerrors.Errorf("Failed to upsert FetchMeta to DB. dbpath: %s, err: %w", viper.GetString("dbpath"), err) 71 | } 72 | 73 | return nil 74 | } 75 | -------------------------------------------------------------------------------- /cmd/debian.go: -------------------------------------------------------------------------------- 1 | package cmd 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 | "github.com/vulsio/gost/db" 13 | "github.com/vulsio/gost/fetcher" 14 | "github.com/vulsio/gost/models" 15 | "github.com/vulsio/gost/util" 16 | ) 17 | 18 | // debianCmd represents the debian command 19 | var debianCmd = &cobra.Command{ 20 | Use: "debian", 21 | Short: "Fetch the CVE information from Debian", 22 | Long: `Fetch the CVE information from Debian`, 23 | RunE: fetchDebian, 24 | } 25 | 26 | func init() { 27 | fetchCmd.AddCommand(debianCmd) 28 | } 29 | 30 | func fetchDebian(_ *cobra.Command, _ []string) (err error) { 31 | if err := util.SetLogger(viper.GetBool("log-to-file"), viper.GetString("log-dir"), viper.GetBool("debug"), viper.GetBool("log-json")); err != nil { 32 | return xerrors.Errorf("Failed to SetLogger. err: %w", err) 33 | } 34 | 35 | log15.Info("Initialize Database") 36 | driver, err := db.NewDB(viper.GetString("dbtype"), viper.GetString("dbpath"), viper.GetBool("debug-sql"), db.Option{}) 37 | if err != nil { 38 | if errors.Is(err, db.ErrDBLocked) { 39 | return xerrors.Errorf("Failed to open DB. Close DB connection before fetching. err: %w", err) 40 | } 41 | return xerrors.Errorf("Failed to open DB. err: %w", err) 42 | } 43 | 44 | fetchMeta, err := driver.GetFetchMeta() 45 | if err != nil { 46 | return xerrors.Errorf("Failed to get FetchMeta from DB. err: %w", err) 47 | } 48 | if fetchMeta.OutDated() { 49 | return xerrors.Errorf("Failed to Insert CVEs into DB. err: SchemaVersion is old. SchemaVersion: %+v", map[string]uint{"latest": models.LatestSchemaVersion, "DB": fetchMeta.SchemaVersion}) 50 | } 51 | // If the fetch fails the first time (without SchemaVersion), the DB needs to be cleaned every time, so insert SchemaVersion. 52 | if err := driver.UpsertFetchMeta(fetchMeta); err != nil { 53 | return xerrors.Errorf("Failed to upsert FetchMeta to DB. err: %w", err) 54 | } 55 | 56 | log15.Info("Fetched all CVEs from Debian") 57 | cveJSONs, err := fetcher.RetrieveDebianCveDetails() 58 | if err != nil { 59 | return err 60 | } 61 | cves := models.ConvertDebian(cveJSONs) 62 | 63 | log15.Info("Fetched", "CVEs", len(cves)) 64 | 65 | log15.Info("Insert Debian CVEs into DB", "db", driver.Name()) 66 | if err := driver.InsertDebian(cves); err != nil { 67 | return xerrors.Errorf("Failed to insert. dbpath: %s, err: %w", viper.GetString("dbpath"), err) 68 | } 69 | 70 | fetchMeta.LastFetchedAt = time.Now() 71 | if err := driver.UpsertFetchMeta(fetchMeta); err != nil { 72 | return xerrors.Errorf("Failed to upsert FetchMeta to DB. dbpath: %s, err: %w", viper.GetString("dbpath"), err) 73 | } 74 | 75 | return nil 76 | } 77 | -------------------------------------------------------------------------------- /cmd/microsoft.go: -------------------------------------------------------------------------------- 1 | package cmd 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 | "github.com/vulsio/gost/db" 13 | "github.com/vulsio/gost/fetcher" 14 | "github.com/vulsio/gost/models" 15 | "github.com/vulsio/gost/util" 16 | ) 17 | 18 | // microsoftCmd represents the microsoft command 19 | var microsoftCmd = &cobra.Command{ 20 | Use: "microsoft", 21 | Short: "Fetch the CVE information from Microsoft", 22 | Long: `Fetch the CVE information from Microsoft`, 23 | RunE: fetchMicrosoft, 24 | } 25 | 26 | func init() { 27 | fetchCmd.AddCommand(microsoftCmd) 28 | } 29 | 30 | func fetchMicrosoft(_ *cobra.Command, _ []string) (err error) { 31 | if err := util.SetLogger(viper.GetBool("log-to-file"), viper.GetString("log-dir"), viper.GetBool("debug"), viper.GetBool("log-json")); err != nil { 32 | return xerrors.Errorf("Failed to SetLogger. err: %w", err) 33 | } 34 | 35 | log15.Info("Initialize Database") 36 | driver, err := db.NewDB(viper.GetString("dbtype"), viper.GetString("dbpath"), viper.GetBool("debug-sql"), db.Option{}) 37 | if err != nil { 38 | if errors.Is(err, db.ErrDBLocked) { 39 | return xerrors.Errorf("Failed to open DB. Close DB connection before fetching. err: %w", err) 40 | } 41 | return xerrors.Errorf("Failed to open DB. err: %w", err) 42 | } 43 | 44 | fetchMeta, err := driver.GetFetchMeta() 45 | if err != nil { 46 | return xerrors.Errorf("Failed to get FetchMeta from DB. err: %w", err) 47 | } 48 | if fetchMeta.OutDated() { 49 | return xerrors.Errorf("Failed to Insert CVEs into DB. err: SchemaVersion is old. SchemaVersion: %+v", map[string]uint{"latest": models.LatestSchemaVersion, "DB": fetchMeta.SchemaVersion}) 50 | } 51 | // If the fetch fails the first time (without SchemaVersion), the DB needs to be cleaned every time, so insert SchemaVersion. 52 | if err := driver.UpsertFetchMeta(fetchMeta); err != nil { 53 | return xerrors.Errorf("Failed to upsert FetchMeta to DB. dbpath: %s, err: %w", viper.GetString("dbpath"), err) 54 | } 55 | 56 | log15.Info("Fetched all CVEs from Microsoft") 57 | vulns, supercedences, err := fetcher.RetrieveMicrosoftCveDetails() 58 | if err != nil { 59 | return err 60 | } 61 | cves, relations := models.ConvertMicrosoft(vulns, supercedences) 62 | 63 | log15.Info("Insert Microsoft CVEs into DB", "db", driver.Name()) 64 | if err := driver.InsertMicrosoft(cves, relations); err != nil { 65 | return xerrors.Errorf("Failed to insert. dbpath: %s, err: %w", viper.GetString("dbpath"), err) 66 | } 67 | 68 | fetchMeta.LastFetchedAt = time.Now() 69 | if err := driver.UpsertFetchMeta(fetchMeta); err != nil { 70 | return xerrors.Errorf("Failed to upsert FetchMeta to DB. dbpath: %s, err: %w", viper.GetString("dbpath"), err) 71 | } 72 | 73 | return nil 74 | } 75 | -------------------------------------------------------------------------------- /cmd/ubuntu.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "path/filepath" 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 | "github.com/vulsio/gost/db" 15 | "github.com/vulsio/gost/fetcher" 16 | "github.com/vulsio/gost/models" 17 | "github.com/vulsio/gost/util" 18 | ) 19 | 20 | // ubuntuCmd represents the ubuntu command 21 | var ubuntuCmd = &cobra.Command{ 22 | Use: "ubuntu", 23 | Short: "Fetch the CVE information from aquasecurity/vuln-list", 24 | Long: `Fetch the CVE information from aquasecurity/vuln-list`, 25 | RunE: fetchUbuntu, 26 | } 27 | 28 | func init() { 29 | fetchCmd.AddCommand(ubuntuCmd) 30 | } 31 | 32 | func fetchUbuntu(_ *cobra.Command, _ []string) (err error) { 33 | if err := util.SetLogger(viper.GetBool("log-to-file"), viper.GetString("log-dir"), viper.GetBool("debug"), viper.GetBool("log-json")); err != nil { 34 | return xerrors.Errorf("Failed to SetLogger. err: %w", err) 35 | } 36 | 37 | cveJSONs, err := fetcher.FetchUbuntuVulnList() 38 | if err != nil { 39 | return xerrors.Errorf("Failed to initialize vulnerability DB. err: %w", err) 40 | } 41 | defer os.RemoveAll(filepath.Join(util.CacheDir(), "vuln-list")) 42 | 43 | log15.Info("Initialize Database") 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. err: 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. dbpath: %s, err: %w", viper.GetString("dbpath"), err) 62 | } 63 | 64 | log15.Info("Insert Ubuntu into DB", "db", driver.Name()) 65 | if err := driver.InsertUbuntu(models.ConvertUbuntu(cveJSONs)); err != nil { 66 | return xerrors.Errorf("Failed to insert. dbpath: %s, err: %w", viper.GetString("dbpath"), err) 67 | } 68 | 69 | fetchMeta.LastFetchedAt = time.Now() 70 | if err := driver.UpsertFetchMeta(fetchMeta); err != nil { 71 | return xerrors.Errorf("Failed to upsert FetchMeta to DB. dbpath: %s, err: %w", viper.GetString("dbpath"), err) 72 | } 73 | 74 | return nil 75 | } 76 | -------------------------------------------------------------------------------- /cmd/redhat.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "path/filepath" 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 | "github.com/vulsio/gost/db" 15 | "github.com/vulsio/gost/fetcher" 16 | "github.com/vulsio/gost/models" 17 | "github.com/vulsio/gost/util" 18 | ) 19 | 20 | // redhatCmd represents the redhat command 21 | var redHatCmd = &cobra.Command{ 22 | Use: "redhat", 23 | Short: "Fetch the CVE information from aquasecurity/vuln-list-redhat", 24 | Long: `Fetch the CVE information from aquasecurity/vuln-list-redhat`, 25 | RunE: fetchRedHat, 26 | } 27 | 28 | func init() { 29 | fetchCmd.AddCommand(redHatCmd) 30 | } 31 | 32 | func fetchRedHat(_ *cobra.Command, _ []string) (err error) { 33 | if err := util.SetLogger(viper.GetBool("log-to-file"), viper.GetString("log-dir"), viper.GetBool("debug"), viper.GetBool("log-json")); err != nil { 34 | return xerrors.Errorf("Failed to SetLogger. err: %w", err) 35 | } 36 | 37 | cveJSONs, err := fetcher.FetchRedHatVulnList() 38 | if err != nil { 39 | return xerrors.Errorf("Failed to initialize vulnerability DB. err: %w", err) 40 | } 41 | defer os.RemoveAll(filepath.Join(util.CacheDir(), "vuln-list-redhat")) 42 | 43 | log15.Info("Initialize Database") 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. err: 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. dbpath: %s, err: %w", viper.GetString("dbpath"), err) 62 | } 63 | 64 | log15.Info("Insert RedHat into DB", "db", driver.Name()) 65 | if err := driver.InsertRedhat(models.ConvertRedhat(cveJSONs)); err != nil { 66 | return xerrors.Errorf("Failed to insert. dbpath: %s, err: %w", viper.GetString("dbpath"), err) 67 | } 68 | 69 | fetchMeta.LastFetchedAt = time.Now() 70 | if err := driver.UpsertFetchMeta(fetchMeta); err != nil { 71 | return xerrors.Errorf("Failed to upsert FetchMeta to DB. dbpath: %s, err: %w", viper.GetString("dbpath"), err) 72 | } 73 | 74 | return nil 75 | } 76 | -------------------------------------------------------------------------------- /models/arch.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // ArchADVJSON : 4 | type ArchADVJSON struct { 5 | Advisories []string `json:"advisories"` 6 | Affected string `json:"affected"` 7 | Fixed *string `json:"fixed"` 8 | Issues []string `json:"issues"` 9 | Name string `json:"name"` 10 | Packages []string `json:"packages"` 11 | Severity string `json:"severity"` 12 | Status string `json:"status"` 13 | Ticket *string `json:"ticket"` 14 | Type string `json:"type"` 15 | } 16 | 17 | // ArchADV : 18 | type ArchADV struct { 19 | ID int64 `json:"-"` 20 | Name string `json:"name" gorm:"type:varchar(255)"` 21 | Packages []ArchPackage `json:"packages"` 22 | Status string `json:"status" gorm:"type:varchar(255)"` 23 | Severity string `json:"severity" gorm:"type:varchar(255)"` 24 | Type string `json:"type" gorm:"type:varchar(255)"` 25 | Affected string `json:"affected" gorm:"type:varchar(255)"` 26 | Fixed *string `json:"fixed" gorm:"type:varchar(255)"` 27 | Ticket *string `json:"ticket" gorm:"type:varchar(255)"` 28 | Issues []ArchIssue `json:"issues"` 29 | Advisories []ArchAdvisory `json:"advisories"` 30 | } 31 | 32 | // ArchPackage : 33 | type ArchPackage struct { 34 | ID int64 `json:"-"` 35 | ArchADVID int64 `json:"-"` 36 | Name string `json:"name" gorm:"type:varchar(255);index:idx_arch_packages_name"` 37 | } 38 | 39 | // ArchIssue : 40 | type ArchIssue struct { 41 | ID int64 `json:"-"` 42 | ArchADVID int64 `json:"-"` 43 | Issue string `json:"issue" gorm:"type:varchar(255);index:idx_arch_issues_issue"` 44 | } 45 | 46 | // ArchAdvisory : 47 | type ArchAdvisory struct { 48 | ID int64 `json:"-"` 49 | ArchADVID int64 `json:"-"` 50 | Advisory string `json:"advisory" gorm:"type:varchar(255)"` 51 | } 52 | 53 | // ConvertArch : 54 | func ConvertArch(advJSONs []ArchADVJSON) []ArchADV { 55 | advs := make([]ArchADV, 0, len(advJSONs)) 56 | for _, aj := range advJSONs { 57 | advs = append(advs, ArchADV{ 58 | Name: aj.Name, 59 | Packages: func() []ArchPackage { 60 | ps := make([]ArchPackage, 0, len(aj.Packages)) 61 | for _, p := range aj.Packages { 62 | ps = append(ps, ArchPackage{Name: p}) 63 | } 64 | return ps 65 | }(), 66 | Status: aj.Status, 67 | Severity: aj.Severity, 68 | Type: aj.Type, 69 | Affected: aj.Affected, 70 | Fixed: aj.Fixed, 71 | Ticket: aj.Ticket, 72 | Issues: func() []ArchIssue { 73 | is := make([]ArchIssue, 0, len(aj.Issues)) 74 | for _, i := range aj.Issues { 75 | is = append(is, ArchIssue{Issue: i}) 76 | } 77 | return is 78 | }(), 79 | Advisories: func() []ArchAdvisory { 80 | as := make([]ArchAdvisory, 0, len(aj.Advisories)) 81 | for _, a := range aj.Advisories { 82 | as = append(as, ArchAdvisory{Advisory: a}) 83 | } 84 | return as 85 | }(), 86 | }) 87 | } 88 | return advs 89 | } 90 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 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/gost/util" 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: "gost", 21 | Short: "Security Tracker", 22 | Long: `Security Tracker`, 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/.gost.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", util.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") 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, "gost.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 ".gost" (without extension). 71 | viper.AddConfigPath(home) 72 | viper.SetConfigName(".gost") 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 | -------------------------------------------------------------------------------- /models/debian.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "maps" 5 | "slices" 6 | ) 7 | 8 | // DebianJSON : 9 | type DebianJSON map[string]DebianCveMap 10 | 11 | // DebianCveMap : 12 | type DebianCveMap map[string]DebianCveJSON 13 | 14 | // DebianCveJSON : 15 | type DebianCveJSON struct { 16 | Scope string `json:"scope"` 17 | Debianbug int `json:"debianbug"` 18 | Description string `json:"description"` 19 | Releases map[string]DebianReleaseJSON `json:"releases"` 20 | } 21 | 22 | // DebianReleaseJSON : 23 | type DebianReleaseJSON struct { 24 | Status string `json:"status"` 25 | Repositories map[string]string `json:"repositories"` 26 | FixedVersion string `json:"fixed_version"` 27 | Urgency string `json:"urgency"` 28 | } 29 | 30 | // DebianCVE : 31 | type DebianCVE struct { 32 | ID int64 `json:"-"` 33 | CveID string `gorm:"index:idx_debian_cves_cveid;type:varchar(255);"` 34 | Scope string `gorm:"type:varchar(255)"` 35 | Description string 36 | Package []DebianPackage 37 | } 38 | 39 | // DebianPackage : 40 | type DebianPackage struct { 41 | ID int64 `json:"-"` 42 | DebianCVEID int64 `json:"-" gorm:"index:idx_debian_packages_debian_cve_id"` 43 | PackageName string `gorm:"type:varchar(255);index:idx_debian_packages_package_name"` 44 | Release []DebianRelease 45 | } 46 | 47 | // DebianRelease : 48 | type DebianRelease struct { 49 | ID int64 `json:"-"` 50 | DebianPackageID int64 `json:"-" gorm:"index:idx_debian_releases_debian_package_id"` 51 | ProductName string `gorm:"type:varchar(255);index:idx_debian_releases_product_name"` 52 | Status string `gorm:"type:varchar(255);index:idx_debian_releases_status"` 53 | FixedVersion string `gorm:"type:varchar(255);"` 54 | Urgency string `gorm:"type:varchar(255);"` 55 | Version string `gorm:"type:varchar(255);"` 56 | } 57 | 58 | // ConvertDebian : 59 | func ConvertDebian(cveJSONs DebianJSON) []DebianCVE { 60 | uniqCve := map[string]DebianCVE{} 61 | for pkgName, cveMap := range cveJSONs { 62 | for cveID, cve := range cveMap { 63 | var releases []DebianRelease 64 | for release, releaseInfo := range cve.Releases { 65 | r := DebianRelease{ 66 | ProductName: release, 67 | Status: releaseInfo.Status, 68 | FixedVersion: releaseInfo.FixedVersion, 69 | Urgency: releaseInfo.Urgency, 70 | Version: releaseInfo.Repositories[release], 71 | } 72 | releases = append(releases, r) 73 | } 74 | 75 | pkg := DebianPackage{ 76 | PackageName: pkgName, 77 | Release: releases, 78 | } 79 | 80 | pkgs := []DebianPackage{pkg} 81 | if oldCve, ok := uniqCve[cveID]; ok { 82 | pkgs = append(pkgs, oldCve.Package...) 83 | } 84 | 85 | uniqCve[cveID] = DebianCVE{ 86 | CveID: cveID, 87 | Scope: cve.Scope, 88 | Description: cve.Description, 89 | Package: pkgs, 90 | } 91 | } 92 | } 93 | return slices.Collect(maps.Values(uniqCve)) 94 | } 95 | -------------------------------------------------------------------------------- /db/db.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "fmt" 5 | "iter" 6 | "time" 7 | 8 | "golang.org/x/xerrors" 9 | 10 | "github.com/vulsio/gost/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 | IsGostModelV1() (bool, error) 21 | GetFetchMeta() (*models.FetchMeta, error) 22 | UpsertFetchMeta(*models.FetchMeta) error 23 | 24 | GetAfterTimeRedhat(time.Time) ([]models.RedhatCVE, error) 25 | GetRedhat(string) (*models.RedhatCVE, error) 26 | GetRedhatMulti([]string) (map[string]models.RedhatCVE, error) 27 | GetUnfixedCvesRedhat(string, string, bool) (map[string]models.RedhatCVE, error) 28 | GetAdvisoriesRedHat() (map[string][]string, error) 29 | GetDebian(string) (*models.DebianCVE, error) 30 | GetDebianMulti([]string) (map[string]models.DebianCVE, error) 31 | GetFixedCvesDebian(string, string) (map[string]models.DebianCVE, error) 32 | GetUnfixedCvesDebian(string, string) (map[string]models.DebianCVE, error) 33 | GetUbuntu(string) (*models.UbuntuCVE, error) 34 | GetUbuntuMulti([]string) (map[string]models.UbuntuCVE, error) 35 | GetFixedCvesUbuntu(string, string) (map[string]models.UbuntuCVE, error) 36 | GetUnfixedCvesUbuntu(string, string) (map[string]models.UbuntuCVE, error) 37 | GetAdvisoriesUbuntu() (map[string][]string, error) 38 | GetMicrosoft(string) (*models.MicrosoftCVE, error) 39 | GetMicrosoftMulti([]string) (map[string]models.MicrosoftCVE, error) 40 | GetExpandKB([]string, []string) ([]string, []string, error) 41 | GetRelatedProducts(string, []string) ([]string, error) 42 | GetFilteredCvesMicrosoft([]string, []string) (map[string]models.MicrosoftCVE, error) 43 | GetAdvisoriesMicrosoft() (map[string][]string, error) 44 | GetArch(string) (*models.ArchADV, error) 45 | GetArchMulti([]string) (map[string]models.ArchADV, error) 46 | GetFixedAdvsArch(string) (map[string]models.ArchADV, error) 47 | GetUnfixedAdvsArch(string) (map[string]models.ArchADV, error) 48 | GetAdvisoriesArch() (map[string][]string, error) 49 | 50 | InsertRedhat(iter.Seq2[models.RedhatCVE, error]) error 51 | InsertDebian([]models.DebianCVE) error 52 | InsertUbuntu(iter.Seq2[models.UbuntuCVE, error]) error 53 | InsertMicrosoft([]models.MicrosoftCVE, []models.MicrosoftKBRelation) error 54 | InsertArch([]models.ArchADV) error 55 | } 56 | 57 | // Option : 58 | type Option struct { 59 | RedisTimeout time.Duration 60 | } 61 | 62 | // NewDB returns db driver 63 | func NewDB(dbType, dbPath string, debugSQL bool, option Option) (driver DB, err error) { 64 | if driver, err = newDB(dbType); err != nil { 65 | return driver, xerrors.Errorf("Failed to new db. err: %w", err) 66 | } 67 | 68 | if err := driver.OpenDB(dbType, dbPath, debugSQL, option); err != nil { 69 | return nil, xerrors.Errorf("Failed to open db. err: %w", err) 70 | } 71 | 72 | isV1, err := driver.IsGostModelV1() 73 | if err != nil { 74 | return nil, xerrors.Errorf("Failed to IsGostModelV1. err: %w", err) 75 | } 76 | if isV1 { 77 | return nil, xerrors.New("Failed to NewDB. Since SchemaVersion is incompatible, delete Database and fetch again.") 78 | } 79 | 80 | if err := driver.MigrateDB(); err != nil { 81 | return driver, xerrors.Errorf("Failed to migrate db. err: %w", err) 82 | } 83 | return driver, nil 84 | } 85 | 86 | func newDB(dbType string) (DB, error) { 87 | switch dbType { 88 | case dialectSqlite3, dialectMysql, dialectPostgreSQL: 89 | return &RDBDriver{name: dbType}, nil 90 | case dialectRedis: 91 | return &RedisDriver{name: dbType}, nil 92 | } 93 | return nil, fmt.Errorf("Invalid database dialect. dbType: %s", dbType) 94 | } 95 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/vulsio/gost 2 | 3 | go 1.24.0 4 | 5 | require ( 6 | github.com/BurntSushi/toml v1.5.0 7 | github.com/aquasecurity/trivy-db v0.0.0-20230622083554-a5a9b0a72b48 8 | github.com/cenkalti/backoff v2.2.1+incompatible 9 | github.com/cheggaaa/pb/v3 v3.1.7 10 | github.com/glebarez/sqlite v1.11.0 11 | github.com/go-redis/redis/v8 v8.11.5 12 | github.com/inconshreveable/log15 v3.0.0-testing.5+incompatible 13 | github.com/labstack/echo/v4 v4.13.4 14 | github.com/mattn/go-runewidth v0.0.19 15 | github.com/mitchellh/go-homedir v1.1.0 16 | github.com/opencontainers/image-spec v1.1.1 17 | github.com/parnurzeal/gorequest v0.3.0 18 | github.com/spf13/cobra v1.10.1 19 | github.com/spf13/viper v1.21.0 20 | go.etcd.io/bbolt v1.4.3 21 | golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 22 | gorm.io/driver/mysql v1.5.5 23 | gorm.io/driver/postgres v1.5.7 24 | gorm.io/gorm v1.25.7 25 | oras.land/oras-go/v2 v2.6.0 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/clipperhouse/uax29/v2 v2.2.0 // indirect 32 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 33 | github.com/dustin/go-humanize v1.0.1 // indirect 34 | github.com/elazarl/goproxy v1.7.2 // indirect 35 | github.com/fatih/color v1.18.0 // indirect 36 | github.com/fsnotify/fsnotify v1.9.0 // indirect 37 | github.com/glebarez/go-sqlite v1.21.2 // indirect 38 | github.com/go-sql-driver/mysql v1.7.1 // indirect 39 | github.com/go-stack/stack v1.8.0 // indirect 40 | github.com/go-viper/mapstructure/v2 v2.4.0 // indirect 41 | github.com/google/go-cmp v0.7.0 // indirect 42 | github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6 // indirect 43 | github.com/google/uuid v1.6.0 // indirect 44 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 45 | github.com/jackc/pgpassfile v1.0.0 // indirect 46 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect 47 | github.com/jackc/pgx/v5 v5.5.4 // indirect 48 | github.com/jackc/puddle/v2 v2.2.1 // indirect 49 | github.com/jinzhu/inflection v1.0.0 // indirect 50 | github.com/jinzhu/now v1.1.5 // indirect 51 | github.com/labstack/gommon v0.4.2 // indirect 52 | github.com/mattn/go-colorable v0.1.14 // indirect 53 | github.com/mattn/go-isatty v0.0.20 // indirect 54 | github.com/moul/http2curl v1.0.0 // indirect 55 | github.com/onsi/gomega v1.34.1 // indirect 56 | github.com/opencontainers/go-digest v1.0.0 // indirect 57 | github.com/pelletier/go-toml/v2 v2.2.4 // indirect 58 | github.com/pkg/errors v0.9.1 // indirect 59 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect 60 | github.com/rogpeppe/go-internal v1.14.1 // indirect 61 | github.com/sagikazarmark/locafero v0.11.0 // indirect 62 | github.com/smartystreets/goconvey v1.7.2 // indirect 63 | github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect 64 | github.com/spf13/afero v1.15.0 // indirect 65 | github.com/spf13/cast v1.10.0 // indirect 66 | github.com/spf13/pflag v1.0.10 // indirect 67 | github.com/subosito/gotenv v1.6.0 // indirect 68 | github.com/valyala/bytebufferpool v1.0.0 // indirect 69 | github.com/valyala/fasttemplate v1.2.2 // indirect 70 | go.yaml.in/yaml/v3 v3.0.4 // indirect 71 | golang.org/x/crypto v0.45.0 // indirect 72 | golang.org/x/net v0.47.0 // indirect 73 | golang.org/x/sync v0.18.0 // indirect 74 | golang.org/x/sys v0.38.0 // indirect 75 | golang.org/x/term v0.37.0 // indirect 76 | golang.org/x/text v0.31.0 // indirect 77 | golang.org/x/time v0.11.0 // indirect 78 | modernc.org/libc v1.22.5 // indirect 79 | modernc.org/mathutil v1.5.0 // indirect 80 | modernc.org/memory v1.5.0 // indirect 81 | modernc.org/sqlite v1.23.1 // indirect 82 | ) 83 | -------------------------------------------------------------------------------- /cmd/redhatapi.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "iter" 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 | "github.com/vulsio/gost/db" 15 | "github.com/vulsio/gost/fetcher" 16 | "github.com/vulsio/gost/models" 17 | "github.com/vulsio/gost/util" 18 | ) 19 | 20 | // redHatAPICmd represents the redhatAPI command 21 | var redHatAPICmd = &cobra.Command{ 22 | Use: "redhatapi", 23 | Short: "Fetch the CVE information from Red Hat API", 24 | Long: `Fetch the CVE information from Red Hat API`, 25 | RunE: fetchRedHatAPI, 26 | } 27 | 28 | func init() { 29 | fetchCmd.AddCommand(redHatAPICmd) 30 | 31 | redHatAPICmd.PersistentFlags().String("after", "1970-01-01", "Fetch CVEs after the specified date (e.g. 2017-01-01)") 32 | _ = viper.BindPFlag("after", redHatAPICmd.PersistentFlags().Lookup("after")) 33 | 34 | redHatAPICmd.PersistentFlags().String("before", "", "Fetch CVEs before the specified date (e.g. 2017-01-01)") 35 | _ = viper.BindPFlag("before", redHatAPICmd.PersistentFlags().Lookup("before")) 36 | 37 | redHatAPICmd.PersistentFlags().Bool("list-only", false, "") 38 | _ = viper.BindPFlag("list-only", redHatAPICmd.PersistentFlags().Lookup("list-only")) 39 | } 40 | 41 | func fetchRedHatAPI(_ *cobra.Command, _ []string) (err error) { 42 | if err := util.SetLogger(viper.GetBool("log-to-file"), viper.GetString("log-dir"), viper.GetBool("debug"), viper.GetBool("log-json")); err != nil { 43 | return xerrors.Errorf("Failed to SetLogger. err: %w", err) 44 | } 45 | 46 | log15.Info("Initialize Database") 47 | driver, err := db.NewDB(viper.GetString("dbtype"), viper.GetString("dbpath"), viper.GetBool("debug-sql"), db.Option{}) 48 | if err != nil { 49 | if errors.Is(err, db.ErrDBLocked) { 50 | return xerrors.Errorf("Failed to open DB. Close DB connection before fetching. err: %w", err) 51 | } 52 | return xerrors.Errorf("Failed to open DB. err: %w", err) 53 | } 54 | 55 | fetchMeta, err := driver.GetFetchMeta() 56 | if err != nil { 57 | return xerrors.Errorf("Failed to get FetchMeta from DB. err: %w", err) 58 | } 59 | if fetchMeta.OutDated() { 60 | return xerrors.Errorf("Failed to Insert CVEs into DB. err: SchemaVersion is old. SchemaVersion: %+v", map[string]uint{"latest": models.LatestSchemaVersion, "DB": fetchMeta.SchemaVersion}) 61 | } 62 | // If the fetch fails the first time (without SchemaVersion), the DB needs to be cleaned every time, so insert SchemaVersion. 63 | if err := driver.UpsertFetchMeta(fetchMeta); err != nil { 64 | return xerrors.Errorf("Failed to upsert FetchMeta to DB. dbpath: %s, err: %w", viper.GetString("dbpath"), err) 65 | } 66 | 67 | log15.Info("Fetch the list of CVEs") 68 | entries, err := fetcher.ListAllRedhatCves( 69 | viper.GetString("before"), viper.GetString("after"), viper.GetInt("wait")) 70 | if err != nil { 71 | return xerrors.Errorf("Failed to fetch the list of CVEs. err: %w", err) 72 | } 73 | resourceURLs := []string{} 74 | for _, entry := range entries { 75 | resourceURLs = append(resourceURLs, entry.ResourceURL) 76 | } 77 | 78 | if viper.GetBool("list-only") { 79 | for _, e := range entries { 80 | fmt.Printf("%s\t%s\n", e.CveID, e.PublicDate) 81 | } 82 | return nil 83 | } 84 | 85 | log15.Info(fmt.Sprintf("Fetched %d CVEs", len(entries))) 86 | cveJSONs, err := fetcher.RetrieveRedhatCveDetails(resourceURLs) 87 | if err != nil { 88 | return xerrors.Errorf("Failed to fetch the CVE details. err: %w", err) 89 | } 90 | 91 | log15.Info("Insert RedHat into DB", "db", driver.Name()) 92 | if err := driver.InsertRedhat(models.ConvertRedhat(func() iter.Seq2[models.RedhatCVEJSON, error] { 93 | return func(yield func(models.RedhatCVEJSON, error) bool) { 94 | for _, cveJSON := range cveJSONs { 95 | if !yield(cveJSON, nil) { 96 | return 97 | } 98 | } 99 | } 100 | }())); err != nil { 101 | return xerrors.Errorf("Failed to insert. dbpath: %s, err: %w", viper.GetString("dbpath"), err) 102 | } 103 | 104 | fetchMeta.LastFetchedAt = time.Now() 105 | if err := driver.UpsertFetchMeta(fetchMeta); err != nil { 106 | return xerrors.Errorf("Failed to upsert FetchMeta to DB. dbpath: %s, err: %w", viper.GetString("dbpath"), err) 107 | } 108 | 109 | return nil 110 | } 111 | -------------------------------------------------------------------------------- /fetcher/redhatapi.go: -------------------------------------------------------------------------------- 1 | package fetcher 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "net/http" 8 | "time" 9 | 10 | "github.com/spf13/viper" 11 | "golang.org/x/xerrors" 12 | 13 | "github.com/vulsio/gost/models" 14 | "github.com/vulsio/gost/util" 15 | ) 16 | 17 | // ListAllRedhatCves returns the list of all CVEs from RedHat API 18 | // https://access.redhat.com/documentation/en-us/red_hat_security_data_api/0.1/html-single/red_hat_security_data_api/#list_all_cves 19 | func ListAllRedhatCves(before, after string, wait int) (entries []models.RedhatEntry, err error) { 20 | for page := 1; ; page++ { 21 | url := fmt.Sprintf("https://access.redhat.com/labs/securitydataapi/cve.json?page=%d&after=%s", page, after) 22 | if before != "" { 23 | url += fmt.Sprintf("&before=%s", before) 24 | 25 | } 26 | resp, err := util.FetchURL(url) 27 | if err != nil { 28 | return entries, xerrors.Errorf("Failed to fetch RedHat CVEs: url: %s, err: %w", url, err) 29 | } 30 | defer resp.Body.Close() 31 | 32 | if resp.StatusCode != http.StatusOK { 33 | return entries, xerrors.Errorf("Failed to fetch RedHat CVEs: url: %s, err: status code: %d", url, resp.StatusCode) 34 | } 35 | 36 | entryList := []models.RedhatEntry{} 37 | if err = json.NewDecoder(resp.Body).Decode(&entryList); err != nil { 38 | return nil, err 39 | } 40 | if len(entryList) == 0 { 41 | break 42 | } 43 | entries = append(entries, entryList...) 44 | time.Sleep(time.Duration(wait) * time.Second) 45 | } 46 | return entries, nil 47 | } 48 | 49 | // GetRedhatCveDetailURL returns CVE detail URL. 50 | func GetRedhatCveDetailURL(cveID string) (url string) { 51 | return fmt.Sprintf("https://access.redhat.com/labs/securitydataapi/cve/%s.json", cveID) 52 | 53 | } 54 | 55 | // RetrieveRedhatCveDetails returns full CVE details from RedHat API 56 | // https://access.redhat.com/documentation/en-us/red_hat_security_data_api/0.1/html-single/red_hat_security_data_api/#retrieve_a_cve 57 | func RetrieveRedhatCveDetails(urls []string) (cves []models.RedhatCVEJSON, err error) { 58 | cveJSONs, err := util.FetchConcurrently(urls, viper.GetInt("threads"), viper.GetInt("wait")) 59 | if err != nil { 60 | return cves, xerrors.Errorf("Failed to fetch cve data from RedHat. err: %w", err) 61 | } 62 | 63 | for _, cveJSON := range cveJSONs { 64 | var cve models.RedhatCVEJSON 65 | if err = json.Unmarshal(cveJSON, &cve); err != nil { 66 | return nil, err 67 | } 68 | switch cve.TempAffectedRelease.(type) { 69 | case []interface{}: 70 | var ar models.RedhatCVEJSONAffectedReleaseArray 71 | if err = json.Unmarshal(cveJSON, &ar); err != nil { 72 | return nil, xerrors.Errorf("Unknown affected_release type err: %w", err) 73 | } 74 | cve.AffectedRelease = ar.AffectedRelease 75 | case map[string]interface{}: 76 | var ar models.RedhatCVEJSONAffectedReleaseObject 77 | if err = json.Unmarshal(cveJSON, &ar); err != nil { 78 | return nil, xerrors.Errorf("Unknown affected_release type err: %w", err) 79 | } 80 | cve.AffectedRelease = []models.RedhatAffectedRelease{ar.AffectedRelease} 81 | case nil: 82 | default: 83 | return nil, errors.New("Unknown affected_release type") 84 | } 85 | 86 | switch cve.TempPackageState.(type) { 87 | case []interface{}: 88 | var ps models.RedhatCVEJSONPackageStateArray 89 | if err = json.Unmarshal(cveJSON, &ps); err != nil { 90 | return nil, xerrors.Errorf("Unknown package_state type err: %w", err) 91 | } 92 | cve.PackageState = ps.PackageState 93 | case map[string]interface{}: 94 | var ps models.RedhatCVEJSONPackageStateObject 95 | if err = json.Unmarshal(cveJSON, &ps); err != nil { 96 | return nil, xerrors.Errorf("Unknown package_state type err: %w", err) 97 | } 98 | cve.PackageState = []models.RedhatPackageState{ps.PackageState} 99 | case nil: 100 | default: 101 | return nil, errors.New("Unknown package_state type") 102 | } 103 | 104 | switch cve.TempMitigation.(type) { 105 | case string: 106 | cve.Mitigation = cve.TempMitigation.(string) 107 | case map[string]interface{}: 108 | var m struct { 109 | Mitigation models.RedhatCVEJSONMitigationObject `json:"mitigation"` 110 | } 111 | if err := json.Unmarshal(cveJSON, &m); err != nil { 112 | return nil, xerrors.Errorf("unknown mitigation type err: %w", err) 113 | } 114 | cve.Mitigation = m.Mitigation.Value 115 | case nil: 116 | default: 117 | return nil, errors.New("Unknown mitigation type") 118 | } 119 | 120 | cves = append(cves, cve) 121 | } 122 | 123 | return cves, nil 124 | } 125 | -------------------------------------------------------------------------------- /cmd/notify.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "iter" 7 | 8 | "github.com/BurntSushi/toml" 9 | "github.com/inconshreveable/log15" 10 | "github.com/spf13/cobra" 11 | "github.com/spf13/viper" 12 | "golang.org/x/xerrors" 13 | 14 | "github.com/vulsio/gost/config" 15 | "github.com/vulsio/gost/db" 16 | "github.com/vulsio/gost/fetcher" 17 | "github.com/vulsio/gost/models" 18 | "github.com/vulsio/gost/notifier" 19 | "github.com/vulsio/gost/util" 20 | ) 21 | 22 | // notifyCmd represents the notify command 23 | var notifyCmd = &cobra.Command{ 24 | Use: "notify", 25 | Short: "Notify update about the specified CVE", 26 | Long: `Notify update about the specified CVE`, 27 | RunE: executeNotify, 28 | } 29 | 30 | func init() { 31 | RootCmd.AddCommand(notifyCmd) 32 | 33 | RootCmd.PersistentFlags().Bool("to-email", false, "Send notification via Email") 34 | _ = viper.BindPFlag("to-email", RootCmd.PersistentFlags().Lookup("to-email")) 35 | 36 | RootCmd.PersistentFlags().Bool("to-slack", false, "Send notification via Slack") 37 | _ = viper.BindPFlag("to-slack", RootCmd.PersistentFlags().Lookup("to-slack")) 38 | } 39 | 40 | func executeNotify(_ *cobra.Command, _ []string) (err error) { 41 | if err := util.SetLogger(viper.GetBool("log-to-file"), viper.GetString("log-dir"), viper.GetBool("debug"), viper.GetBool("log-json")); err != nil { 42 | return xerrors.Errorf("Failed to SetLogger. err: %w", err) 43 | } 44 | 45 | log15.Info("Load toml config") 46 | var conf config.Config 47 | if _, err = toml.DecodeFile("config.toml", &conf); err != nil { 48 | return err 49 | } 50 | return notifyRedhat(conf) 51 | } 52 | 53 | func notifyRedhat(conf config.Config) error { 54 | watchCveURL := []string{} 55 | for cveID := range conf.Redhat { 56 | watchCveURL = append(watchCveURL, fetcher.GetRedhatCveDetailURL(cveID)) 57 | } 58 | 59 | log15.Info(fmt.Sprintf("Fetched %d CVEs", len(watchCveURL))) 60 | cveJSONs, err := fetcher.RetrieveRedhatCveDetails(watchCveURL) 61 | if err != nil { 62 | return err 63 | } 64 | 65 | log15.Info("Initialize Database") 66 | driver, err := db.NewDB(viper.GetString("dbtype"), viper.GetString("dbpath"), viper.GetBool("debug-sql"), db.Option{}) 67 | if err != nil { 68 | if errors.Is(err, db.ErrDBLocked) { 69 | return xerrors.Errorf("Failed to open DB. Close DB connection before fetching. err: %w", err) 70 | } 71 | return xerrors.Errorf("Failed to open DB. err: %w", err) 72 | } 73 | 74 | fetchMeta, err := driver.GetFetchMeta() 75 | if err != nil { 76 | return xerrors.Errorf("Failed to get FetchMeta from DB. err: %w", err) 77 | } 78 | if fetchMeta.OutDated() { 79 | return xerrors.Errorf("Failed to notify command. err: SchemaVersion is old. SchemaVersion: %+v", map[string]uint{"latest": models.LatestSchemaVersion, "DB": fetchMeta.SchemaVersion}) 80 | } 81 | 82 | for cve, err := range models.ConvertRedhat(func() iter.Seq2[models.RedhatCVEJSON, error] { 83 | return func(yield func(models.RedhatCVEJSON, error) bool) { 84 | for _, cveJSON := range cveJSONs { 85 | if !yield(cveJSON, nil) { 86 | return 87 | } 88 | } 89 | } 90 | }()) { 91 | if err != nil { 92 | return xerrors.Errorf("Failed to convert RedHat CVE JSON. err: %w", err) 93 | } 94 | 95 | // Select CVE information from DB 96 | c, err := driver.GetRedhat(cve.Name) 97 | if err != nil { 98 | return err 99 | } 100 | notifier.ClearIDRedhat(c) 101 | 102 | cve.Cvss3.Cvss3BaseScore = "10 (This is dummy)" 103 | cve.ThreatSeverity = "High (This is dummy)" 104 | body := notifier.DiffRedhat(c, &cve, conf.Redhat[cve.Name]) 105 | if body != "" { 106 | subject := fmt.Sprintf("%s Update %s", conf.EMail.SubjectPrefix, cve.Name) 107 | body = fmt.Sprintf("%s\nhttps://access.redhat.com/security/cve/%s\n========================================================\n", 108 | cve.Name, cve.Name) + body 109 | if err := notify(subject, body, conf); err != nil { 110 | return err 111 | } 112 | } 113 | } 114 | return nil 115 | } 116 | 117 | func notify(subject, body string, conf config.Config) (err error) { 118 | if viper.GetBool("to-email") { 119 | sender := notifier.NewEMailSender(conf.EMail) 120 | log15.Info("Send e-mail") 121 | if err = sender.Send(subject, body); err != nil { 122 | return xerrors.Errorf("Failed to send e-mail. err: %w", err) 123 | } 124 | } 125 | 126 | if viper.GetBool("to-slack") { 127 | log15.Info("Send slack") 128 | if err = notifier.SendSlack(body, conf.Slack); err != nil { 129 | return xerrors.Errorf("Failed to send to Slack. err: %w", err) 130 | } 131 | } 132 | return nil 133 | } 134 | -------------------------------------------------------------------------------- /db/arch.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "os" 8 | "slices" 9 | 10 | "github.com/cheggaaa/pb/v3" 11 | "github.com/spf13/viper" 12 | "github.com/vulsio/gost/models" 13 | "golang.org/x/xerrors" 14 | "gorm.io/gorm" 15 | ) 16 | 17 | // GetArch : 18 | func (r *RDBDriver) GetArch(advID string) (*models.ArchADV, error) { 19 | var a models.ArchADV 20 | if err := r.conn. 21 | Preload("Packages"). 22 | Preload("Issues"). 23 | Preload("Advisories"). 24 | Where(&models.ArchADV{Name: advID}). 25 | First(&a).Error; err != nil { 26 | if errors.Is(err, gorm.ErrRecordNotFound) { 27 | return nil, nil 28 | } 29 | return nil, xerrors.Errorf("Failed to find first record by %s. err: %w", advID, err) 30 | } 31 | return &a, nil 32 | } 33 | 34 | // GetArchMulti : 35 | func (r *RDBDriver) GetArchMulti(advIDs []string) (map[string]models.ArchADV, error) { 36 | m := make(map[string]models.ArchADV) 37 | for _, id := range advIDs { 38 | a, err := r.GetArch(id) 39 | if err != nil { 40 | return nil, xerrors.Errorf("Failed to get Arch. err: %w", err) 41 | } 42 | if a != nil { 43 | m[id] = *a 44 | } 45 | } 46 | return m, nil 47 | } 48 | 49 | // InsertArch : 50 | func (r *RDBDriver) InsertArch(advs []models.ArchADV) error { 51 | if err := r.deleteAndInsertArch(advs); err != nil { 52 | return xerrors.Errorf("Failed to insert Arch Advisory data. err: %w", err) 53 | } 54 | return nil 55 | } 56 | 57 | func (r *RDBDriver) deleteAndInsertArch(advs []models.ArchADV) (err error) { 58 | bar := pb.StartNew(len(advs)).SetWriter(func() io.Writer { 59 | if viper.GetBool("log-json") { 60 | return io.Discard 61 | } 62 | return os.Stderr 63 | }()) 64 | tx := r.conn.Begin() 65 | 66 | defer func() { 67 | if err != nil { 68 | tx.Rollback() 69 | return 70 | } 71 | tx.Commit() 72 | }() 73 | 74 | // Delete all old records 75 | for _, table := range []interface{}{models.ArchAdvisory{}, models.ArchIssue{}, models.ArchPackage{}, models.ArchADV{}} { 76 | if err := tx.Session(&gorm.Session{AllowGlobalUpdate: true}).Delete(table).Error; err != nil { 77 | return xerrors.Errorf("Failed to delete old records. err: %w", err) 78 | } 79 | } 80 | 81 | batchSize := viper.GetInt("batch-size") 82 | if batchSize < 1 { 83 | return fmt.Errorf("Failed to set batch-size. err: batch-size option is not set properly") 84 | } 85 | 86 | for chunk := range slices.Chunk(advs, batchSize) { 87 | if err = tx.Create(chunk).Error; err != nil { 88 | return xerrors.Errorf("Failed to insert. err: %w", err) 89 | } 90 | bar.Add(len(chunk)) 91 | } 92 | bar.Finish() 93 | 94 | return nil 95 | } 96 | 97 | // GetUnfixedAdvsArch : 98 | func (r *RDBDriver) GetUnfixedAdvsArch(pkgName string) (map[string]models.ArchADV, error) { 99 | return r.getAdvsArchWithFixStatus(pkgName, "Vulnerable") 100 | } 101 | 102 | // GetFixedAdvsArch : 103 | func (r *RDBDriver) GetFixedAdvsArch(pkgName string) (map[string]models.ArchADV, error) { 104 | return r.getAdvsArchWithFixStatus(pkgName, "Fixed") 105 | } 106 | 107 | func (r *RDBDriver) getAdvsArchWithFixStatus(pkgName, fixStatus string) (map[string]models.ArchADV, error) { 108 | var as []models.ArchADV 109 | if err := r.conn. 110 | Joins("JOIN arch_packages ON arch_packages.arch_adv_id = arch_advs.id AND arch_packages.name = ?", pkgName). 111 | Preload("Packages"). 112 | Preload("Issues"). 113 | Preload("Advisories"). 114 | Where(&models.ArchADV{Status: fixStatus}). 115 | Find(&as).Error; err != nil { 116 | return nil, xerrors.Errorf("Failed to find advisory by pkgname: %s, fix status: %s. err: %w", pkgName, fixStatus, err) 117 | } 118 | 119 | m := make(map[string]models.ArchADV) 120 | for _, a := range as { 121 | m[a.Name] = a 122 | } 123 | return m, nil 124 | } 125 | 126 | // GetAdvisoriesArch gets AdvisoryID: []CVE IDs 127 | func (r *RDBDriver) GetAdvisoriesArch() (map[string][]string, error) { 128 | m := make(map[string][]string) 129 | var as []models.ArchADV 130 | // the maximum value of a host parameter number is SQLITE_MAX_VARIABLE_NUMBER, which defaults to 999 for SQLite versions prior to 3.32.0 (2020-05-22) or 32766 for SQLite versions after 3.32.0. 131 | // https://www.sqlite.org/limits.html Maximum Number Of Host Parameters In A Single SQL Statement 132 | if err := r.conn.Preload("Issues").FindInBatches(&as, 999, func(_ *gorm.DB, _ int) error { 133 | for _, a := range as { 134 | for _, i := range a.Issues { 135 | m[a.Name] = append(m[a.Name], i.Issue) 136 | } 137 | } 138 | return nil 139 | }).Error; err != nil { 140 | return nil, xerrors.Errorf("Failed to find Arch. err: %w", err) 141 | } 142 | 143 | return m, nil 144 | } 145 | -------------------------------------------------------------------------------- /cmd/register.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "os" 9 | "os/exec" 10 | "runtime" 11 | "strings" 12 | "time" 13 | 14 | "github.com/BurntSushi/toml" 15 | "github.com/inconshreveable/log15" 16 | runewidth "github.com/mattn/go-runewidth" 17 | "github.com/spf13/cobra" 18 | "github.com/spf13/viper" 19 | "golang.org/x/xerrors" 20 | 21 | "github.com/vulsio/gost/config" 22 | "github.com/vulsio/gost/db" 23 | "github.com/vulsio/gost/models" 24 | "github.com/vulsio/gost/util" 25 | ) 26 | 27 | // registerCmd represents the register command 28 | var registerCmd = &cobra.Command{ 29 | Use: "register", 30 | Short: "Register CVEs to monitor", 31 | Long: `Register CVEs to monitor`, 32 | RunE: executeRegister, 33 | } 34 | 35 | func init() { 36 | RootCmd.AddCommand(registerCmd) 37 | 38 | registerCmd.PersistentFlags().String("select-cmd", "fzf", "Select command") 39 | _ = viper.BindPFlag("select-cmd", registerCmd.PersistentFlags().Lookup("select-cmd")) 40 | 41 | registerCmd.PersistentFlags().String("select-option", "--reverse", "Select command options") 42 | _ = viper.BindPFlag("select-option", registerCmd.PersistentFlags().Lookup("select-option")) 43 | 44 | registerCmd.PersistentFlags().String("select-after", "", "Show CVEs after the specified date (e.g. 2017-01-01) (default: 30 days ago)") 45 | _ = viper.BindPFlag("select-after", registerCmd.PersistentFlags().Lookup("select-after")) 46 | } 47 | 48 | func executeRegister(_ *cobra.Command, _ []string) (err error) { 49 | if err := util.SetLogger(viper.GetBool("log-to-file"), viper.GetString("log-dir"), viper.GetBool("debug"), viper.GetBool("log-json")); err != nil { 50 | return xerrors.Errorf("Failed to SetLogger. err: %w", err) 51 | } 52 | 53 | log15.Info("Validate command-line options") 54 | afterOption := viper.GetString("select-after") 55 | var after time.Time 56 | if afterOption != "" { 57 | if after, err = time.Parse("2006-01-02", afterOption); err != nil { 58 | return xerrors.Errorf("Failed to parse --select-after. err: %w", err) 59 | } 60 | } else { 61 | now := time.Now() 62 | after = now.Add(time.Duration(-1) * 24 * 30 * time.Hour) 63 | } 64 | 65 | log15.Info("Load toml config") 66 | var conf config.Config 67 | filename := "config.toml" 68 | if _, err = os.Stat(filename); err == nil { 69 | _, err = toml.DecodeFile("config.toml", &conf) 70 | if err != nil { 71 | return err 72 | } 73 | } else { 74 | conf.Redhat = map[string]config.RedhatWatchCve{} 75 | } 76 | 77 | log15.Info("Initialize Database") 78 | driver, err := db.NewDB(viper.GetString("dbtype"), viper.GetString("dbpath"), viper.GetBool("debug-sql"), db.Option{}) 79 | if err != nil { 80 | if errors.Is(err, db.ErrDBLocked) { 81 | return xerrors.Errorf("Failed to open DB. Close DB connection before fetching. err: %w", err) 82 | } 83 | return xerrors.Errorf("Failed to open DB. err: %w", err) 84 | } 85 | 86 | fetchMeta, err := driver.GetFetchMeta() 87 | if err != nil { 88 | return xerrors.Errorf("Failed to get FetchMeta from DB. err: %w", err) 89 | } 90 | if fetchMeta.OutDated() { 91 | return xerrors.Errorf("Failed to register command. err: SchemaVersion is old. SchemaVersion: %+v", map[string]uint{"latest": models.LatestSchemaVersion, "DB": fetchMeta.SchemaVersion}) 92 | } 93 | 94 | log15.Info("Select all RedHat CVEs") 95 | allRedhat, err := driver.GetAfterTimeRedhat(after) 96 | if err != nil { 97 | return err 98 | } 99 | 100 | allRedhatText := []string{} 101 | for _, redhat := range allRedhat { 102 | if redhat.Name == "" { 103 | continue 104 | } 105 | allRedhatText = append(allRedhatText, fmt.Sprintf("%-16s | %-10s | %-3s | %-24s | %s", redhat.Name, redhat.ThreatSeverity, 106 | redhat.Cvss3.Cvss3BaseScore, runewidth.Truncate(redhat.GetPackages(","), 20, "..."), runewidth.Truncate(redhat.GetDetail(""), 120, "..."))) 107 | } 108 | selectedLine, err := filter(allRedhatText) 109 | if err != nil { 110 | return err 111 | } 112 | 113 | cves := []string{} 114 | for _, line := range selectedLine { 115 | split := strings.Split(line, "|") 116 | if len(split) < 2 { 117 | continue 118 | } 119 | cves = append(cves, strings.TrimSpace(split[0])) 120 | } 121 | 122 | log15.Info("Register CVEs to watch list") 123 | for _, cve := range cves { 124 | _, ok := conf.Redhat[cve] 125 | if !ok { 126 | conf.Redhat[cve] = config.RedhatWatchCve{} 127 | } 128 | } 129 | if err = save(conf); err != nil { 130 | return xerrors.Errorf("Failed to save the selected CVEs. err: %w", err) 131 | } 132 | 133 | return err 134 | } 135 | 136 | func run(command string, r io.Reader, w io.Writer) error { 137 | var cmd *exec.Cmd 138 | if runtime.GOOS == "windows" { 139 | cmd = exec.Command("cmd", "/c", command) 140 | } else { 141 | cmd = exec.Command("sh", "-c", command) 142 | } 143 | cmd.Stderr = os.Stderr 144 | cmd.Stdout = w 145 | cmd.Stdin = r 146 | return cmd.Run() 147 | } 148 | 149 | func filter(cves []string) (results []string, err error) { 150 | var buf bytes.Buffer 151 | selectCmd := fmt.Sprintf("%s %s", 152 | viper.GetString("select-cmd"), viper.GetString("select-option")) 153 | err = run(selectCmd, strings.NewReader(strings.Join(cves, "\n")), &buf) 154 | if err != nil { 155 | return nil, nil 156 | } 157 | 158 | lines := strings.Split(strings.TrimSpace(buf.String()), "\n") 159 | 160 | return lines, nil 161 | } 162 | 163 | func save(conf config.Config) error { 164 | confFile := "config.toml" 165 | f, err := os.Create(confFile) 166 | if err != nil { 167 | return xerrors.Errorf("Failed to save config file. err: %w", err) 168 | } 169 | defer f.Close() 170 | return toml.NewEncoder(f).Encode(conf) 171 | } 172 | -------------------------------------------------------------------------------- /db/debian.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "os" 8 | "slices" 9 | 10 | "github.com/cheggaaa/pb/v3" 11 | "github.com/spf13/viper" 12 | "golang.org/x/xerrors" 13 | "gorm.io/gorm" 14 | 15 | "github.com/vulsio/gost/models" 16 | ) 17 | 18 | // GetDebian : 19 | func (r *RDBDriver) GetDebian(cveID string) (*models.DebianCVE, error) { 20 | c := models.DebianCVE{} 21 | if err := r.conn.Where(&models.DebianCVE{CveID: cveID}).First(&c).Error; err != nil { 22 | if errors.Is(err, gorm.ErrRecordNotFound) { 23 | return nil, nil 24 | } 25 | return nil, xerrors.Errorf("Failed to get Debian. err: %w", err) 26 | } 27 | 28 | if err := r.conn.Model(&c).Association("Package").Find(&c.Package); err != nil { 29 | return nil, xerrors.Errorf("Failed to get Debian.Package. err: %w", err) 30 | } 31 | 32 | newPkg := []models.DebianPackage{} 33 | for _, pkg := range c.Package { 34 | if err := r.conn.Model(&pkg).Association("Release").Find(&pkg.Release); err != nil { 35 | return nil, xerrors.Errorf("Failed to get Debian.Package.Release. err: %w", err) 36 | } 37 | newPkg = append(newPkg, pkg) 38 | } 39 | c.Package = newPkg 40 | return &c, nil 41 | } 42 | 43 | // GetDebianMulti : 44 | func (r *RDBDriver) GetDebianMulti(cveIDs []string) (map[string]models.DebianCVE, error) { 45 | m := map[string]models.DebianCVE{} 46 | for _, cveID := range cveIDs { 47 | cve, err := r.GetDebian(cveID) 48 | if err != nil { 49 | return nil, err 50 | } 51 | if cve != nil { 52 | m[cve.CveID] = *cve 53 | } 54 | } 55 | return m, nil 56 | } 57 | 58 | // InsertDebian : 59 | func (r *RDBDriver) InsertDebian(cves []models.DebianCVE) (err error) { 60 | if err = r.deleteAndInsertDebian(cves); err != nil { 61 | return xerrors.Errorf("Failed to insert Debian CVE data. err: %w", err) 62 | } 63 | return nil 64 | } 65 | func (r *RDBDriver) deleteAndInsertDebian(cves []models.DebianCVE) (err error) { 66 | bar := pb.StartNew(len(cves)).SetWriter(func() io.Writer { 67 | if viper.GetBool("log-json") { 68 | return io.Discard 69 | } 70 | return os.Stderr 71 | }()) 72 | tx := r.conn.Begin() 73 | 74 | defer func() { 75 | if err != nil { 76 | tx.Rollback() 77 | return 78 | } 79 | tx.Commit() 80 | }() 81 | 82 | // Delete all old records 83 | for _, table := range []interface{}{models.DebianRelease{}, models.DebianPackage{}, models.DebianCVE{}} { 84 | if err := tx.Session(&gorm.Session{AllowGlobalUpdate: true}).Delete(table).Error; err != nil { 85 | return xerrors.Errorf("Failed to delete old records. err: %w", err) 86 | } 87 | } 88 | 89 | batchSize := viper.GetInt("batch-size") 90 | if batchSize < 1 { 91 | return fmt.Errorf("Failed to set batch-size. err: batch-size option is not set properly") 92 | } 93 | 94 | for chunk := range slices.Chunk(cves, batchSize) { 95 | if err = tx.Create(chunk).Error; err != nil { 96 | return xerrors.Errorf("Failed to insert. err: %w", err) 97 | } 98 | bar.Add(len(chunk)) 99 | } 100 | bar.Finish() 101 | 102 | return nil 103 | } 104 | 105 | var debVerCodename = map[string]string{ 106 | "7": "wheezy", 107 | "8": "jessie", 108 | "9": "stretch", 109 | "10": "buster", 110 | "11": "bullseye", 111 | "12": "bookworm", 112 | "13": "trixie", 113 | "14": "forky", 114 | "15": "duke", 115 | } 116 | 117 | // GetUnfixedCvesDebian gets the CVEs related to debian_release.status = 'open', major, pkgName. 118 | func (r *RDBDriver) GetUnfixedCvesDebian(major, pkgName string) (map[string]models.DebianCVE, error) { 119 | return r.getCvesDebianWithFixStatus(major, pkgName, "open") 120 | } 121 | 122 | // GetFixedCvesDebian gets the CVEs related to debian_release.status = 'resolved', major, pkgName. 123 | func (r *RDBDriver) GetFixedCvesDebian(major, pkgName string) (map[string]models.DebianCVE, error) { 124 | return r.getCvesDebianWithFixStatus(major, pkgName, "resolved") 125 | } 126 | 127 | func (r *RDBDriver) getCvesDebianWithFixStatus(major, pkgName, fixStatus string) (map[string]models.DebianCVE, error) { 128 | codeName, ok := debVerCodename[major] 129 | if !ok { 130 | return nil, xerrors.Errorf("Failed to convert from major version to codename. err: Debian %s is not supported yet", major) 131 | } 132 | 133 | type Result struct { 134 | DebianCveID int64 135 | } 136 | 137 | results := []Result{} 138 | err := r.conn. 139 | Table("debian_packages"). 140 | Select("debian_cve_id"). 141 | Where("package_name = ?", pkgName). 142 | Scan(&results).Error 143 | 144 | if err != nil { 145 | if fixStatus == "open" { 146 | return nil, xerrors.Errorf("Failed to get unfixed cves of Debian: %w", err) 147 | } 148 | return nil, xerrors.Errorf("Failed to get fixed cves of Debian. err: %w", err) 149 | } 150 | 151 | m := map[string]models.DebianCVE{} 152 | for _, res := range results { 153 | debcve := models.DebianCVE{} 154 | if err := r.conn. 155 | Preload("Package.Release", "status = ? AND product_name = ?", fixStatus, codeName). 156 | Preload("Package", "package_name = ?", pkgName). 157 | Where(&models.DebianCVE{ID: res.DebianCveID}). 158 | First(&debcve).Error; err != nil { 159 | if errors.Is(err, gorm.ErrRecordNotFound) { 160 | return nil, xerrors.Errorf("Failed to get DebianCVE. DB relationship may be broken, use `$ gost fetch debian` to recreate DB. err: %w", err) 161 | } 162 | return nil, xerrors.Errorf("Failed to get DebianCVE. DebianCveID: %d, err: %w", res.DebianCveID, err) 163 | } 164 | 165 | if len(debcve.Package) != 0 { 166 | for _, pkg := range debcve.Package { 167 | if len(pkg.Release) != 0 { 168 | m[debcve.CveID] = debcve 169 | } 170 | 171 | } 172 | } 173 | } 174 | 175 | return m, nil 176 | } 177 | -------------------------------------------------------------------------------- /models/ubuntu.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "iter" 5 | "strings" 6 | "time" 7 | ) 8 | 9 | // UbuntuCVEJSON : 10 | type UbuntuCVEJSON struct { 11 | PublicDateAtUSN time.Time 12 | CRD time.Time 13 | Candidate string 14 | PublicDate time.Time 15 | References []string 16 | Description string 17 | UbuntuDescription string 18 | Notes []string 19 | Bugs []string 20 | Priority string 21 | DiscoveredBy string 22 | AssignedTo string 23 | Patches map[string]map[string]UbuntuPatchJSON 24 | UpstreamLinks map[string][]string 25 | } 26 | 27 | // UbuntuPatchJSON : 28 | type UbuntuPatchJSON struct { 29 | Status string 30 | Note string 31 | } 32 | 33 | // UbuntuCVE : 34 | type UbuntuCVE struct { 35 | ID int64 `json:"-"` 36 | 37 | PublicDateAtUSN time.Time `json:"public_date_at_usn"` 38 | CRD time.Time `json:"crd"` 39 | Candidate string `json:"candidate" gorm:"type:varchar(255);index:idx_ubuntu_cve_candidate"` 40 | PublicDate time.Time `json:"public_date"` 41 | References []UbuntuReference `json:"references"` 42 | Description string `json:"description" gorm:"type:text"` 43 | UbuntuDescription string `json:"ubuntu_description" gorm:"type:text"` 44 | Notes []UbuntuNote `json:"notes"` 45 | Bugs []UbuntuBug `json:"bugs"` 46 | Priority string `json:"priority" gorm:"type:varchar(255)"` 47 | DiscoveredBy string `json:"discovered_by" gorm:"type:text"` 48 | AssignedTo string `json:"assigned_to" gorm:"type:varchar(255)"` 49 | Patches []UbuntuPatch `json:"patches"` 50 | Upstreams []UbuntuUpstream `json:"upstreams"` 51 | } 52 | 53 | // UbuntuReference : 54 | type UbuntuReference struct { 55 | ID int64 `json:"-"` 56 | UbuntuCVEID int64 `json:"-" gorm:"index:idx_ubuntu_reference_ubuntu_cve_id"` 57 | Reference string `json:"reference" gorm:"type:text"` 58 | } 59 | 60 | // UbuntuNote : 61 | type UbuntuNote struct { 62 | ID int64 `json:"-"` 63 | UbuntuCVEID int64 `json:"-" gorm:"index:idx_ubuntu_note_ubuntu_cve_id"` 64 | Note string `json:"note" gorm:"type:text"` 65 | } 66 | 67 | // UbuntuBug : 68 | type UbuntuBug struct { 69 | ID int64 `json:"-"` 70 | UbuntuCVEID int64 `json:"-" gorm:"index:idx_ubuntu_bug_ubuntu_cve_id"` 71 | Bug string `json:"bug" gorm:"type:text"` 72 | } 73 | 74 | // UbuntuPatch : 75 | type UbuntuPatch struct { 76 | ID int64 `json:"-"` 77 | UbuntuCVEID int64 `json:"-" gorm:"index:idx_ubuntu_patch_ubuntu_cve_id"` 78 | PackageName string `json:"package_name" gorm:"type:varchar(255);index:idx_ubuntu_patch_package_name"` 79 | ReleasePatches []UbuntuReleasePatch `json:"release_patches"` 80 | } 81 | 82 | // UbuntuReleasePatch : 83 | type UbuntuReleasePatch struct { 84 | ID int64 `json:"-"` 85 | UbuntuPatchID int64 `json:"-" gorm:"index:idx_ubuntu_release_patch_ubuntu_patch_id"` 86 | ReleaseName string `json:"release_name" gorm:"type:varchar(255);index:idx_ubuntu_release_patch_release_name"` 87 | Status string `json:"status" gorm:"type:varchar(255);index:idx_ubuntu_release_patch_status"` 88 | Note string `json:"note" gorm:"type:varchar(255)"` 89 | } 90 | 91 | // UbuntuUpstream : 92 | type UbuntuUpstream struct { 93 | ID int64 `json:"-"` 94 | UbuntuCVEID int64 `json:"-" gorm:"index:idx_ubuntu_upstream_ubuntu_cve_id"` 95 | PackageName string `json:"package_name" gorm:"type:varchar(255)"` 96 | UpstreamLinks []UbuntuUpstreamLink `json:"upstream_links"` 97 | } 98 | 99 | // UbuntuUpstreamLink : 100 | type UbuntuUpstreamLink struct { 101 | ID int64 `json:"-"` 102 | UbuntuUpstreamID int64 `json:"-" gorm:"index:idx_ubuntu_upstream_link_ubuntu_upstream_id"` 103 | Link string `json:"link" gorm:"type:text"` 104 | } 105 | 106 | // ConvertUbuntu : 107 | func ConvertUbuntu(cveJSONs iter.Seq2[UbuntuCVEJSON, error]) iter.Seq2[UbuntuCVE, error] { 108 | return func(yield func(UbuntuCVE, error) bool) { 109 | for cve, err := range cveJSONs { 110 | if err != nil { 111 | if !yield(UbuntuCVE{}, err) { 112 | return 113 | } 114 | continue 115 | } 116 | 117 | if strings.Contains(cve.Description, "** REJECT **") { 118 | continue 119 | } 120 | 121 | if cve.PublicDateAtUSN.Equal(time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC)) { 122 | cve.PublicDateAtUSN = time.Date(1000, time.January, 1, 0, 0, 0, 0, time.UTC) 123 | } 124 | 125 | if cve.CRD.Equal(time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC)) { 126 | cve.CRD = time.Date(1000, time.January, 1, 0, 0, 0, 0, time.UTC) 127 | } 128 | 129 | if cve.PublicDate.Equal(time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC)) { 130 | cve.PublicDate = time.Date(1000, time.January, 1, 0, 0, 0, 0, time.UTC) 131 | } 132 | 133 | references := []UbuntuReference{} 134 | for _, r := range cve.References { 135 | references = append(references, UbuntuReference{Reference: r}) 136 | } 137 | 138 | notes := []UbuntuNote{} 139 | for _, n := range cve.Notes { 140 | notes = append(notes, UbuntuNote{Note: n}) 141 | } 142 | 143 | bugs := []UbuntuBug{} 144 | for _, b := range cve.Bugs { 145 | bugs = append(bugs, UbuntuBug{Bug: b}) 146 | } 147 | 148 | patches := []UbuntuPatch{} 149 | for pkgName, p := range cve.Patches { 150 | var releasePatch []UbuntuReleasePatch 151 | for release, patch := range p { 152 | releasePatch = append(releasePatch, UbuntuReleasePatch{ReleaseName: release, Status: patch.Status, Note: patch.Note}) 153 | } 154 | patches = append(patches, UbuntuPatch{PackageName: pkgName, ReleasePatches: releasePatch}) 155 | } 156 | 157 | upstreams := []UbuntuUpstream{} 158 | for pkgName, u := range cve.UpstreamLinks { 159 | links := []UbuntuUpstreamLink{} 160 | for _, link := range u { 161 | links = append(links, UbuntuUpstreamLink{Link: link}) 162 | } 163 | upstreams = append(upstreams, UbuntuUpstream{PackageName: pkgName, UpstreamLinks: links}) 164 | } 165 | 166 | c := UbuntuCVE{ 167 | PublicDateAtUSN: cve.PublicDateAtUSN, 168 | CRD: cve.CRD, 169 | Candidate: cve.Candidate, 170 | PublicDate: cve.PublicDate, 171 | References: references, 172 | Description: cve.Description, 173 | UbuntuDescription: cve.UbuntuDescription, 174 | Notes: notes, 175 | Bugs: bugs, 176 | Priority: cve.Priority, 177 | DiscoveredBy: cve.DiscoveredBy, 178 | AssignedTo: cve.AssignedTo, 179 | Patches: patches, 180 | Upstreams: upstreams, 181 | } 182 | if !yield(c, nil) { 183 | return 184 | } 185 | } 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /util/util.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "iter" 7 | "maps" 8 | "net/http" 9 | "net/url" 10 | "os" 11 | "path/filepath" 12 | "runtime" 13 | "slices" 14 | "strings" 15 | "time" 16 | 17 | "github.com/cheggaaa/pb/v3" 18 | "github.com/inconshreveable/log15" 19 | "github.com/spf13/viper" 20 | "golang.org/x/xerrors" 21 | ) 22 | 23 | // Unique return unique elements 24 | func Unique[T comparable](s []T) []T { 25 | m := map[T]struct{}{} 26 | for _, v := range s { 27 | m[v] = struct{}{} 28 | } 29 | return slices.Collect(maps.Keys(m)) 30 | } 31 | 32 | // GenWorkers generate workers 33 | func GenWorkers(num, wait int) chan<- func() { 34 | tasks := make(chan func()) 35 | for i := 0; i < num; i++ { 36 | go func() { 37 | for f := range tasks { 38 | f() 39 | time.Sleep(time.Duration(wait) * time.Second) 40 | } 41 | }() 42 | } 43 | return tasks 44 | } 45 | 46 | // GetDefaultLogDir returns default log directory 47 | func GetDefaultLogDir() string { 48 | defaultLogDir := "/var/log/gost" 49 | if runtime.GOOS == "windows" { 50 | defaultLogDir = filepath.Join(os.Getenv("APPDATA"), "gost") 51 | } 52 | return defaultLogDir 53 | } 54 | 55 | // TrimSpaceNewline deletes space character and newline character(CR/LF) 56 | func TrimSpaceNewline(str string) string { 57 | str = strings.TrimSpace(str) 58 | return strings.Trim(str, "\r\n") 59 | } 60 | 61 | // FetchURL returns HTTP response 62 | func FetchURL(fetchURL string) (*http.Response, error) { 63 | client := &http.Client{} 64 | if proxy := viper.GetString("http-proxy"); proxy != "" { 65 | proxyURL, err := url.Parse(proxy) 66 | if err != nil { 67 | return nil, xerrors.Errorf("Failed to parse proxy URL. proxy: %s, err: %w", proxy, err) 68 | } 69 | client.Transport = &http.Transport{Proxy: http.ProxyURL(proxyURL)} 70 | } 71 | 72 | req, err := http.NewRequest("GET", fetchURL, nil) 73 | if err != nil { 74 | return nil, xerrors.Errorf("Failed to create HTTP request. url: %s, err: %w", fetchURL, err) 75 | } 76 | 77 | resp, err := client.Do(req) 78 | if err != nil { 79 | return nil, xerrors.Errorf("Failed to send HTTP request. url: %s, err: %w", fetchURL, err) 80 | } 81 | 82 | return resp, nil 83 | } 84 | 85 | // FetchConcurrently fetches concurrently 86 | func FetchConcurrently(urls []string, concurrency, wait int) (responses [][]byte, err error) { 87 | reqChan := make(chan string, len(urls)) 88 | resChan := make(chan []byte, len(urls)) 89 | errChan := make(chan error, len(urls)) 90 | defer close(reqChan) 91 | defer close(resChan) 92 | defer close(errChan) 93 | 94 | go func() { 95 | for _, url := range urls { 96 | reqChan <- url 97 | } 98 | }() 99 | 100 | bar := pb.StartNew(len(urls)).SetWriter(func() io.Writer { 101 | if viper.GetBool("log-json") { 102 | return io.Discard 103 | } 104 | return os.Stderr 105 | }()) 106 | tasks := GenWorkers(concurrency, wait) 107 | for range urls { 108 | tasks <- func() { 109 | url := <-reqChan 110 | var err error 111 | for i := 1; i <= 3; i++ { 112 | var res []byte 113 | res, err = func() ([]byte, error) { 114 | resp, err := FetchURL(url) 115 | if err != nil { 116 | return nil, xerrors.Errorf("Failed to fetch URL. url: %s, err: %w", url, err) 117 | } 118 | defer resp.Body.Close() 119 | 120 | if resp.StatusCode != http.StatusOK { 121 | return nil, xerrors.Errorf("Failed to fetch URL. url: %s, err: status code: %d", url, resp.StatusCode) 122 | } 123 | 124 | bs, err := io.ReadAll(resp.Body) 125 | if err != nil { 126 | return nil, xerrors.Errorf("Failed to read response body. url: %s, err: %w", url, err) 127 | } 128 | 129 | return bs, nil 130 | }() 131 | if err != nil { 132 | time.Sleep(time.Duration(i*2) * time.Second) 133 | continue 134 | } 135 | resChan <- res 136 | return 137 | } 138 | errChan <- err 139 | } 140 | bar.Increment() 141 | } 142 | bar.Finish() 143 | 144 | errs := []error{} 145 | timeout := time.After(10 * 60 * time.Second) 146 | for range urls { 147 | select { 148 | case res := <-resChan: 149 | responses = append(responses, res) 150 | case err := <-errChan: 151 | errs = append(errs, err) 152 | case <-timeout: 153 | return nil, fmt.Errorf("Timeout Fetching URL") 154 | } 155 | } 156 | if 0 < len(errs) { 157 | return nil, fmt.Errorf("%s", errs) 158 | 159 | } 160 | return responses, nil 161 | } 162 | 163 | // SetLogger set logger 164 | func SetLogger(logToFile bool, logDir string, debug, logJSON bool) error { 165 | stderrHandler := log15.StderrHandler 166 | logFormat := log15.LogfmtFormat() 167 | if logJSON { 168 | logFormat = log15.JsonFormatEx(false, true) 169 | stderrHandler = log15.StreamHandler(os.Stderr, logFormat) 170 | } 171 | 172 | lvlHandler := log15.LvlFilterHandler(log15.LvlInfo, stderrHandler) 173 | if debug { 174 | lvlHandler = log15.LvlFilterHandler(log15.LvlDebug, stderrHandler) 175 | } 176 | 177 | var handler log15.Handler 178 | if logToFile { 179 | if _, err := os.Stat(logDir); err != nil { 180 | if os.IsNotExist(err) { 181 | if err := os.Mkdir(logDir, 0700); err != nil { 182 | return xerrors.Errorf("Failed to create log directory. err: %w", err) 183 | } 184 | } else { 185 | return xerrors.Errorf("Failed to check log directory. err: %w", err) 186 | } 187 | } 188 | 189 | logPath := filepath.Join(logDir, "gost.log") 190 | if _, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644); err != nil { 191 | return xerrors.Errorf("Failed to open a log file. err: %w", err) 192 | } 193 | handler = log15.MultiHandler( 194 | log15.Must.FileHandler(logPath, logFormat), 195 | lvlHandler, 196 | ) 197 | } else { 198 | handler = lvlHandler 199 | } 200 | log15.Root().SetHandler(handler) 201 | return nil 202 | } 203 | 204 | // Major returns major version 205 | func Major(osVer string) (majorVersion string) { 206 | return strings.Split(osVer, ".")[0] 207 | } 208 | 209 | // CacheDir return cache dir path string 210 | func CacheDir() string { 211 | tmpDir, err := os.UserCacheDir() 212 | if err != nil { 213 | tmpDir = os.TempDir() 214 | } 215 | return filepath.Join(tmpDir, "gost") 216 | } 217 | 218 | // Chunk chunks the sequence into n-sized chunks 219 | // Note: slices.Chunk doesn't support iterators as of Go 1.23. 220 | // https://pkg.go.dev/slices#Chunk 221 | func Chunk[T any](s iter.Seq2[T, error], n int) iter.Seq2[[]T, error] { 222 | return func(yield func([]T, error) bool) { 223 | if n < 1 { 224 | if !yield(nil, xerrors.New("cannot be less than 1")) { 225 | return 226 | } 227 | } 228 | 229 | chunk := make([]T, 0, n) 230 | for t, err := range s { 231 | if err != nil { 232 | if !yield(nil, err) { 233 | return 234 | } 235 | continue 236 | } 237 | chunk = append(chunk, t) 238 | if len(chunk) != n { 239 | continue 240 | } 241 | 242 | if !yield(chunk, nil) { 243 | return 244 | } 245 | chunk = chunk[:0] 246 | } 247 | 248 | if len(chunk) > 0 { 249 | if !yield(chunk, nil) { 250 | return 251 | } 252 | } 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /db/rdb.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "log" 7 | "os" 8 | "time" 9 | 10 | "github.com/glebarez/sqlite" 11 | "golang.org/x/xerrors" 12 | "gorm.io/driver/mysql" 13 | "gorm.io/driver/postgres" 14 | "gorm.io/gorm" 15 | "gorm.io/gorm/logger" 16 | 17 | "github.com/vulsio/gost/config" 18 | "github.com/vulsio/gost/models" 19 | ) 20 | 21 | // Supported DB dialects. 22 | const ( 23 | dialectSqlite3 = "sqlite3" 24 | dialectMysql = "mysql" 25 | dialectPostgreSQL = "postgres" 26 | ) 27 | 28 | // RDBDriver is Driver for RDB 29 | type RDBDriver struct { 30 | name string 31 | conn *gorm.DB 32 | } 33 | 34 | // https://github.com/mattn/go-sqlite3/blob/edc3bb69551dcfff02651f083b21f3366ea2f5ab/error.go#L18-L66 35 | type errNo int 36 | 37 | type sqliteError struct { 38 | Code errNo /* The error code returned by SQLite */ 39 | } 40 | 41 | // result codes from http://www.sqlite.org/c3ref/c_abort.html 42 | var ( 43 | errBusy = errNo(5) /* The database file is locked */ 44 | errLocked = errNo(6) /* A table in the database is locked */ 45 | ) 46 | 47 | // ErrDBLocked : 48 | var ErrDBLocked = xerrors.New("database is locked") 49 | 50 | // Name return db name 51 | func (r *RDBDriver) Name() string { 52 | return r.name 53 | } 54 | 55 | // OpenDB opens Database 56 | func (r *RDBDriver) OpenDB(dbType, dbPath string, debugSQL bool, _ Option) (err error) { 57 | gormConfig := gorm.Config{ 58 | DisableForeignKeyConstraintWhenMigrating: true, 59 | Logger: logger.New( 60 | log.New(os.Stderr, "\r\n", log.LstdFlags), 61 | logger.Config{ 62 | LogLevel: logger.Silent, 63 | }, 64 | ), 65 | } 66 | 67 | if debugSQL { 68 | gormConfig.Logger = logger.New( 69 | log.New(os.Stderr, "\r\n", log.LstdFlags), 70 | logger.Config{ 71 | SlowThreshold: time.Second, 72 | LogLevel: logger.Info, 73 | Colorful: true, 74 | }, 75 | ) 76 | } 77 | 78 | switch r.name { 79 | case dialectSqlite3: 80 | r.conn, err = gorm.Open(sqlite.Open(dbPath), &gormConfig) 81 | if err != nil { 82 | parsedErr, marshalErr := json.Marshal(err) 83 | if marshalErr != nil { 84 | return xerrors.Errorf("Failed to marshal err. err: %w", marshalErr) 85 | } 86 | 87 | var errMsg sqliteError 88 | if unmarshalErr := json.Unmarshal(parsedErr, &errMsg); unmarshalErr != nil { 89 | return xerrors.Errorf("Failed to unmarshal. err: %w", unmarshalErr) 90 | } 91 | 92 | switch errMsg.Code { 93 | case errBusy, errLocked: 94 | return xerrors.Errorf("Failed to open DB. dbtype: %s, dbpath: %s, err: %w", dbType, dbPath, ErrDBLocked) 95 | default: 96 | return xerrors.Errorf("Failed to open DB. dbtype: %s, dbpath: %s, err: %w", dbType, dbPath, err) 97 | } 98 | } 99 | 100 | r.conn.Exec("PRAGMA foreign_keys = ON") 101 | case dialectMysql: 102 | r.conn, err = gorm.Open(mysql.Open(dbPath), &gormConfig) 103 | if err != nil { 104 | return xerrors.Errorf("Failed to open DB. dbtype: %s, dbpath: %s, err: %w", dbType, dbPath, err) 105 | } 106 | case dialectPostgreSQL: 107 | r.conn, err = gorm.Open(postgres.Open(dbPath), &gormConfig) 108 | if err != nil { 109 | return xerrors.Errorf("Failed to open DB. dbtype: %s, dbpath: %s, err: %w", dbType, dbPath, err) 110 | } 111 | default: 112 | return xerrors.Errorf("Not Supported DB dialects. r.name: %s", r.name) 113 | } 114 | return nil 115 | } 116 | 117 | // CloseDB close Database 118 | func (r *RDBDriver) CloseDB() (err error) { 119 | if r.conn == nil { 120 | return 121 | } 122 | sqlDB, err := r.conn.DB() 123 | if err != nil { 124 | return xerrors.Errorf("Failed to get DB Object. err : %w", err) 125 | } 126 | return sqlDB.Close() 127 | } 128 | 129 | // MigrateDB migrates Database 130 | func (r *RDBDriver) MigrateDB() error { 131 | if err := r.conn.AutoMigrate( 132 | &models.FetchMeta{}, 133 | 134 | &models.RedhatCVE{}, 135 | &models.RedhatDetail{}, 136 | &models.RedhatReference{}, 137 | &models.RedhatBugzilla{}, 138 | &models.RedhatCvss{}, 139 | &models.RedhatCvss3{}, 140 | &models.RedhatAffectedRelease{}, 141 | &models.RedhatPackageState{}, 142 | 143 | &models.DebianCVE{}, 144 | &models.DebianPackage{}, 145 | &models.DebianRelease{}, 146 | 147 | &models.UbuntuCVE{}, 148 | &models.UbuntuReference{}, 149 | &models.UbuntuNote{}, 150 | &models.UbuntuBug{}, 151 | &models.UbuntuPatch{}, 152 | &models.UbuntuReleasePatch{}, 153 | &models.UbuntuUpstream{}, 154 | &models.UbuntuUpstreamLink{}, 155 | 156 | &models.MicrosoftCVE{}, 157 | &models.MicrosoftProduct{}, 158 | &models.MicrosoftScoreSet{}, 159 | &models.MicrosoftKB{}, 160 | &models.MicrosoftKBRelation{}, 161 | &models.MicrosoftSupersededBy{}, 162 | 163 | &models.ArchADV{}, 164 | &models.ArchPackage{}, 165 | &models.ArchIssue{}, 166 | &models.ArchAdvisory{}, 167 | ); err != nil { 168 | switch r.name { 169 | case dialectSqlite3: 170 | if r.name == dialectSqlite3 { 171 | parsedErr, marshalErr := json.Marshal(err) 172 | if marshalErr != nil { 173 | return xerrors.Errorf("Failed to marshal err. err: %w", marshalErr) 174 | } 175 | 176 | var errMsg sqliteError 177 | if unmarshalErr := json.Unmarshal(parsedErr, &errMsg); unmarshalErr != nil { 178 | return xerrors.Errorf("Failed to unmarshal. err: %w", unmarshalErr) 179 | } 180 | 181 | switch errMsg.Code { 182 | case errBusy, errLocked: 183 | return xerrors.Errorf("Failed to migrate. err: %w", ErrDBLocked) 184 | default: 185 | return xerrors.Errorf("Failed to migrate. err: %w", err) 186 | } 187 | } 188 | case dialectMysql, dialectPostgreSQL: 189 | return xerrors.Errorf("Failed to migrate. err: %w", err) 190 | default: 191 | return xerrors.Errorf("Not Supported DB dialects. r.name: %s", r.name) 192 | } 193 | } 194 | 195 | return nil 196 | } 197 | 198 | // IsGostModelV1 determines if the DB was created at the time of Gost Model v1 199 | func (r *RDBDriver) IsGostModelV1() (bool, error) { 200 | if r.conn.Migrator().HasTable(&models.FetchMeta{}) { 201 | return false, nil 202 | } 203 | 204 | var ( 205 | count int64 206 | err error 207 | ) 208 | switch r.name { 209 | case dialectSqlite3: 210 | err = r.conn.Table("sqlite_master").Where("type = ?", "table").Count(&count).Error 211 | case dialectMysql: 212 | err = r.conn.Table("information_schema.tables").Where("table_schema = ?", r.conn.Migrator().CurrentDatabase()).Count(&count).Error 213 | case dialectPostgreSQL: 214 | err = r.conn.Table("pg_tables").Where("schemaname = ?", "public").Count(&count).Error 215 | } 216 | 217 | if count > 0 { 218 | return true, nil 219 | } 220 | return false, err 221 | } 222 | 223 | // GetFetchMeta get FetchMeta from Database 224 | func (r *RDBDriver) GetFetchMeta() (fetchMeta *models.FetchMeta, err error) { 225 | if err = r.conn.Take(&fetchMeta).Error; err != nil { 226 | if !errors.Is(err, gorm.ErrRecordNotFound) { 227 | return nil, err 228 | } 229 | return &models.FetchMeta{GostRevision: config.Revision, SchemaVersion: models.LatestSchemaVersion, LastFetchedAt: time.Date(1000, time.January, 1, 0, 0, 0, 0, time.UTC)}, nil 230 | } 231 | 232 | return fetchMeta, nil 233 | } 234 | 235 | // UpsertFetchMeta upsert FetchMeta to Database 236 | func (r *RDBDriver) UpsertFetchMeta(fetchMeta *models.FetchMeta) error { 237 | fetchMeta.GostRevision = config.Revision 238 | fetchMeta.SchemaVersion = models.LatestSchemaVersion 239 | return r.conn.Save(fetchMeta).Error 240 | } 241 | -------------------------------------------------------------------------------- /notifier/redhat.go: -------------------------------------------------------------------------------- 1 | package notifier 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strings" 7 | 8 | "github.com/vulsio/gost/config" 9 | "github.com/vulsio/gost/models" 10 | ) 11 | 12 | // ClearIDRedhat : 13 | func ClearIDRedhat(cve *models.RedhatCVE) { 14 | cve.ID = 0 15 | cve.Bugzilla.RedhatCVEID = 0 16 | cve.Cvss.RedhatCVEID = 0 17 | cve.Cvss3.RedhatCVEID = 0 18 | 19 | affectedReleases := cve.AffectedRelease 20 | cve.AffectedRelease = []models.RedhatAffectedRelease{} 21 | for _, a := range affectedReleases { 22 | a.RedhatCVEID = 0 23 | cve.AffectedRelease = append(cve.AffectedRelease, a) 24 | } 25 | 26 | packageState := cve.PackageState 27 | cve.PackageState = []models.RedhatPackageState{} 28 | for _, p := range packageState { 29 | p.RedhatCVEID = 0 30 | cve.PackageState = append(cve.PackageState, p) 31 | } 32 | 33 | details := cve.Details 34 | cve.Details = []models.RedhatDetail{} 35 | for _, d := range details { 36 | d.RedhatCVEID = 0 37 | cve.Details = append(cve.Details, d) 38 | } 39 | 40 | references := cve.References 41 | cve.References = []models.RedhatReference{} 42 | for _, r := range references { 43 | r.RedhatCVEID = 0 44 | cve.References = append(cve.References, r) 45 | } 46 | 47 | } 48 | 49 | // DiffRedhat returns the difference between the old and new CVE information 50 | func DiffRedhat(before, after *models.RedhatCVE, config config.RedhatWatchCve) (body string) { 51 | if config.ThreatSeverity { 52 | if before.ThreatSeverity != after.ThreatSeverity { 53 | body += fmt.Sprintf("\nThreat Secirity\n------------------\n[old]\n%v\n\n[new]\n%v\n", 54 | before.ThreatSeverity, after.ThreatSeverity) 55 | } 56 | } 57 | 58 | if config.Statement { 59 | if before.Statement != after.Statement { 60 | body += fmt.Sprintf("\nStatement\n------------------\n[old]\n%v\n[new]\n\n%v\n\n", 61 | before.Statement, after.Statement) 62 | } 63 | } 64 | 65 | if config.Acknowledgement { 66 | if before.Acknowledgement != after.Acknowledgement { 67 | body += fmt.Sprintf("\nAcknowledgement\n------------------\n[old]\n%v\n\n[new]\n%v\n\n", 68 | before.Acknowledgement, after.Acknowledgement) 69 | } 70 | } 71 | 72 | if config.Mitigation { 73 | if before.Mitigation != after.Mitigation { 74 | body += fmt.Sprintf("\nMitigation\n------------------\n[old]\n%v\n\n[new]\n%v\n\n", 75 | before.Mitigation, after.Mitigation) 76 | return 77 | } 78 | } 79 | 80 | if config.Bugzilla { 81 | if !reflect.DeepEqual(before.Bugzilla, after.Bugzilla) { 82 | body += fmt.Sprintf(` 83 | Bugzilla 84 | ------------------ 85 | [old] 86 | BugzillaID: %s 87 | Descriptiion: %s 88 | URL: %s 89 | 90 | [new] 91 | BugzillaID: %s 92 | Descriptiion: %s 93 | URL: %s 94 | `, 95 | before.Bugzilla.BugzillaID, before.Bugzilla.Description, before.Bugzilla.URL, 96 | after.Bugzilla.BugzillaID, after.Bugzilla.Description, after.Bugzilla.URL) 97 | } 98 | } 99 | 100 | if config.Cvss { 101 | if !reflect.DeepEqual(before.Cvss, after.Cvss) { 102 | body += fmt.Sprintf(` 103 | CVSS 104 | ------------------ 105 | [old] 106 | Base Score: %s 107 | Vector: %s 108 | Status: %s 109 | 110 | [new] 111 | Base Score: %s 112 | Vector: %s 113 | Status: %s 114 | `, 115 | before.Cvss.CvssBaseScore, before.Cvss.CvssScoringVector, before.Cvss.Status, 116 | after.Cvss.CvssBaseScore, after.Cvss.CvssScoringVector, after.Cvss.Status) 117 | } 118 | } 119 | 120 | if config.Cvss3 { 121 | if !reflect.DeepEqual(before.Cvss3, after.Cvss3) { 122 | body += fmt.Sprintf(` 123 | CVSSv3 124 | ------------------ 125 | [old] 126 | Base Score: %s 127 | Vector: %s 128 | Status: %s 129 | 130 | [new] 131 | Base Score: %s 132 | Vector: %s 133 | Status: %s 134 | `, 135 | before.Cvss3.Cvss3BaseScore, before.Cvss3.Cvss3ScoringVector, before.Cvss3.Status, 136 | after.Cvss3.Cvss3BaseScore, after.Cvss3.Cvss3ScoringVector, after.Cvss3.Status) 137 | } 138 | } 139 | 140 | if config.AffectedRelease && (len(before.AffectedRelease) > 0 || len(after.AffectedRelease) > 0) { 141 | oldAffectedRelease := map[string]models.RedhatAffectedRelease{} 142 | for _, r := range before.AffectedRelease { 143 | oldAffectedRelease[r.ProductName+"#"+r.Package] = r 144 | } 145 | 146 | newAffectedRelease := map[string]models.RedhatAffectedRelease{} 147 | for _, r := range after.AffectedRelease { 148 | newAffectedRelease[r.ProductName+"#"+r.Package] = r 149 | } 150 | 151 | for key, nar := range newAffectedRelease { 152 | isNew := false 153 | 154 | oar, ok := oldAffectedRelease[key] 155 | if ok { 156 | if !reflect.DeepEqual(oar, nar) { 157 | isNew = true 158 | } 159 | } else { 160 | isNew = true 161 | } 162 | 163 | if !isNew { 164 | continue 165 | } 166 | 167 | body += fmt.Sprintf(` 168 | Affected Release 169 | ------------------ 170 | [old] 171 | Product Name: %s 172 | Advisory: %s 173 | Package: %s 174 | CPE: %s 175 | Release Date: %s 176 | 177 | [new] 178 | Product Name: %s 179 | Advisory: %s 180 | Package: %s 181 | CPE: %s 182 | Release Date: %s 183 | `, 184 | oar.ProductName, oar.Advisory, oar.Package, oar.Cpe, oar.ReleaseDate, 185 | nar.ProductName, nar.Advisory, nar.Package, nar.Cpe, nar.ReleaseDate) 186 | } 187 | } 188 | 189 | if config.PackageState && (len(before.PackageState) > 0 || len(after.PackageState) > 0) { 190 | oldPackageState := map[string]models.RedhatPackageState{} 191 | for _, s := range before.PackageState { 192 | oldPackageState[s.ProductName+"#"+s.PackageName] = s 193 | } 194 | 195 | newPackageState := map[string]models.RedhatPackageState{} 196 | for _, s := range after.PackageState { 197 | newPackageState[s.ProductName+"#"+s.PackageName] = s 198 | } 199 | 200 | for key, nps := range newPackageState { 201 | isNew := false 202 | 203 | ops, ok := oldPackageState[key] 204 | if ok { 205 | if !reflect.DeepEqual(ops, nps) { 206 | isNew = true 207 | } 208 | } else { 209 | isNew = true 210 | } 211 | 212 | if !isNew { 213 | continue 214 | } 215 | 216 | body += fmt.Sprintf(` 217 | Package State 218 | ------------------ 219 | [old] 220 | Product Name: %s 221 | Fix State: %s 222 | Package Name: %s 223 | 224 | [new] 225 | Product Name: %s 226 | Fix State: %s 227 | Package Name: %s 228 | `, 229 | ops.ProductName, ops.FixState, ops.PackageName, 230 | nps.ProductName, nps.FixState, nps.PackageName) 231 | } 232 | 233 | } 234 | 235 | if config.Reference && (len(before.References) > 0 || len(after.References) > 0) { 236 | if !reflect.DeepEqual(before.References, after.References) { 237 | ors := []string{} 238 | for _, r := range before.References { 239 | ors = append(ors, r.Reference) 240 | } 241 | 242 | nrs := []string{} 243 | for _, r := range after.References { 244 | nrs = append(nrs, r.Reference) 245 | } 246 | body += fmt.Sprintf(` 247 | Reference 248 | ------------------ 249 | [old] 250 | %s 251 | 252 | [new] 253 | %s 254 | `, 255 | strings.Join(ors, "\n"), strings.Join(nrs, "\n")) 256 | return 257 | } 258 | } 259 | 260 | if config.Details && (len(before.Details) > 0 || len(after.Details) > 0) { 261 | if !reflect.DeepEqual(before.Details, after.Details) { 262 | ods := []string{} 263 | for _, d := range before.Details { 264 | ods = append(ods, d.Detail) 265 | } 266 | 267 | nds := []string{} 268 | for _, d := range after.Details { 269 | nds = append(nds, d.Detail) 270 | } 271 | 272 | body += fmt.Sprintf(` 273 | Detail 274 | ------------------ 275 | [old] 276 | %s 277 | 278 | [new] 279 | %s 280 | `, 281 | strings.Join(ods, "\n"), strings.Join(nds, "\n")) 282 | return 283 | } 284 | } 285 | 286 | return body 287 | 288 | } 289 | -------------------------------------------------------------------------------- /models/microsoft.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "slices" 5 | "strings" 6 | "time" 7 | ) 8 | 9 | // MicrosoftVulnerability : 10 | type MicrosoftVulnerability struct { 11 | CveID string `json:"CVEID"` 12 | Title string `json:"Title"` 13 | Description string `json:"Description"` 14 | FAQs []string `json:"FAQs"` 15 | Tag string `json:"Tag"` 16 | CNA string `json:"CNA"` 17 | ExploitStatus string `json:"ExploitStatus"` 18 | Mitigation string `json:"Mitigation"` 19 | Workaround string `json:"Workaround"` 20 | Products []struct { 21 | ProductID string `json:"ProductID"` 22 | Name string `json:"Name"` 23 | Impact string `json:"Impact"` 24 | Severity string `json:"Severity"` 25 | ScoreSet struct { 26 | BaseScore string `json:"BaseScore"` 27 | TemporalScore string `json:"TemporalScore"` 28 | Vector string `json:"Vector"` 29 | } `json:"ScoreSet,omitempty"` 30 | KBs []struct { 31 | Article string `json:"Article"` 32 | RestartRequired string `json:"RestartRequired"` 33 | SubType string `json:"SubType"` 34 | FixedBuild string `json:"FixedBuild"` 35 | ArticleURL string `json:"ArticleURL"` 36 | DownloadURL string `json:"DownloadURL"` 37 | } `json:"KBs,omitempty"` 38 | } `json:"Products"` 39 | URL string `json:"URL"` 40 | Acknowledgments []struct { 41 | Name string `json:"Name"` 42 | } `json:"Acknowledgments"` 43 | Revisions []revision `json:"Revisions"` 44 | } 45 | 46 | type revision struct { 47 | Number string `json:"Number"` 48 | Date string `json:"Date"` 49 | Description string `json:"Description"` 50 | } 51 | 52 | // MicrosoftSupercedence : 53 | type MicrosoftSupercedence struct { 54 | KBID string `json:"KBID"` 55 | UpdateID string `json:"UpdateID"` 56 | Product string `json:"Product"` 57 | Supersededby struct { 58 | KBIDs []string `json:"KBIDs"` 59 | UpdateIDs []string `json:"UpdateIDs"` 60 | } `json:"Supersededby"` 61 | } 62 | 63 | // MicrosoftCVE : 64 | type MicrosoftCVE struct { 65 | ID int64 `json:"-"` 66 | CveID string `json:"cve_id" gorm:"type:varchar(255);index:idx_microsoft_cves_cveid"` 67 | Title string `json:"title" gorm:"type:text"` 68 | Description string `json:"description" gorm:"type:text"` 69 | FAQ string `json:"faq" gorm:"type:text"` 70 | Tag string `json:"tag" gorm:"type:varchar(255)"` 71 | CNA string `json:"cna" gorm:"type:varchar(255)"` 72 | ExploitStatus string `json:"exploit_status" gorm:"type:varchar(255)"` 73 | Mitigation string `json:"mitigation" gorm:"type:text"` 74 | Workaround string `json:"workaround" gorm:"type:text"` 75 | Products []MicrosoftProduct `json:"products"` 76 | URL string `json:"url" gorm:"type:varchar(255)"` 77 | Acknowledgments string `json:"acknowledgments" gorm:"type:text"` 78 | PublishDate time.Time `json:"publish_date"` 79 | LastUpdateDate time.Time `json:"last_update_date"` 80 | } 81 | 82 | // MicrosoftProduct : 83 | type MicrosoftProduct struct { 84 | ID int64 `json:"-"` 85 | MicrosoftCVEID int64 `json:"-" gorm:"index:idx_microsoft_product_microsoft_cve_id"` 86 | ProductID string `json:"product_id" gorm:"type:varchar(255)"` 87 | Name string `json:"name" gorm:"type:varchar(255)"` 88 | Impact string `json:"impact" gorm:"type:varchar(255)"` 89 | Severity string `json:"severity" gorm:"type:varchar(255)"` 90 | ScoreSet MicrosoftScoreSet `json:"score_set"` 91 | KBs []MicrosoftKB `json:"kbs"` 92 | } 93 | 94 | // MicrosoftScoreSet : 95 | type MicrosoftScoreSet struct { 96 | ID int64 `json:"-"` 97 | MicrosoftProductID int64 `json:"-" gorm:"index:idx_microsoft_score_set_microsoft_product_id"` 98 | BaseScore string `json:"base_score" gorm:"type:varchar(255)"` 99 | TemporalScore string `json:"temporal_score" gorm:"type:varchar(255)"` 100 | Vector string `json:"vector" gorm:"type:varchar(255)"` 101 | } 102 | 103 | // MicrosoftKB : 104 | type MicrosoftKB struct { 105 | ID int64 `json:"-"` 106 | MicrosoftProductID int64 `json:"-" gorm:"index:idx_microsoft_kb_microsoft_product_id"` 107 | Article string `json:"article" gorm:"type:varchar(255);index:idx_microsoft_kb_article"` 108 | RestartRequired string `json:"restart_required" gorm:"type:varchar(255)"` 109 | SubType string `json:"sub_type" gorm:"type:varchar(255)"` 110 | FixedBuild string `json:"fixed_build" gorm:"type:varchar(255)"` 111 | ArticleURL string `json:"article_url" gorm:"type:varchar(255)"` 112 | DownloadURL string `json:"download_url" gorm:"type:varchar(255)"` 113 | } 114 | 115 | // MicrosoftKBRelation : 116 | type MicrosoftKBRelation struct { 117 | ID int64 `json:"-"` 118 | KBID string `json:"kbid" gorm:"type:varchar(255);index:idx_microsoft_relation_kb_id"` 119 | SupersededBy []MicrosoftSupersededBy 120 | } 121 | 122 | // MicrosoftSupersededBy : 123 | type MicrosoftSupersededBy struct { 124 | ID int64 `json:"-"` 125 | MicrosoftKBRelationID int64 `json:"-" gorm:"index:idx_microsoft_superseded_by_microsoft_kb_relation_id"` 126 | KBID string `json:"kbid" gorm:"type:varchar(255);index:idx_microsoft_superseded_by_kb_id"` 127 | } 128 | 129 | // ConvertMicrosoft : 130 | func ConvertMicrosoft(vulns []MicrosoftVulnerability, supercedences []MicrosoftSupercedence) ([]MicrosoftCVE, []MicrosoftKBRelation) { 131 | cves := []MicrosoftCVE{} 132 | for _, v := range vulns { 133 | cve := MicrosoftCVE{ 134 | CveID: v.CveID, 135 | Title: v.Title, 136 | Description: v.Description, 137 | FAQ: strings.Join(v.FAQs, "\n"), 138 | Tag: v.Tag, 139 | CNA: v.CNA, 140 | ExploitStatus: v.ExploitStatus, 141 | Mitigation: v.Mitigation, 142 | Workaround: v.Workaround, 143 | URL: v.URL, 144 | PublishDate: time.Date(1000, time.January, 1, 0, 0, 0, 0, time.UTC), 145 | LastUpdateDate: time.Date(1000, time.January, 1, 0, 0, 0, 0, time.UTC), 146 | } 147 | 148 | for _, p := range v.Products { 149 | product := MicrosoftProduct{ 150 | ProductID: p.ProductID, 151 | Name: p.Name, 152 | Impact: p.Impact, 153 | Severity: p.Severity, 154 | KBs: []MicrosoftKB{}, 155 | } 156 | if p.ScoreSet.BaseScore != "" || p.ScoreSet.TemporalScore != "" || p.ScoreSet.Vector != "" { 157 | product.ScoreSet = MicrosoftScoreSet{ 158 | BaseScore: p.ScoreSet.BaseScore, 159 | TemporalScore: p.ScoreSet.TemporalScore, 160 | Vector: p.ScoreSet.Vector, 161 | } 162 | } 163 | for _, kb := range p.KBs { 164 | product.KBs = append(product.KBs, MicrosoftKB{ 165 | Article: kb.Article, 166 | RestartRequired: kb.RestartRequired, 167 | SubType: kb.SubType, 168 | FixedBuild: kb.FixedBuild, 169 | ArticleURL: kb.ArticleURL, 170 | DownloadURL: kb.DownloadURL, 171 | }) 172 | } 173 | cve.Products = append(cve.Products, product) 174 | } 175 | 176 | as := []string{} 177 | for _, a := range v.Acknowledgments { 178 | as = append(as, a.Name) 179 | } 180 | cve.Acknowledgments = strings.Join(as, ";") 181 | 182 | revs := []time.Time{} 183 | for _, r := range v.Revisions { 184 | t, err := time.Parse("2006-01-02T15:04:05", strings.TrimSuffix(r.Date, "Z")) 185 | if err == nil { 186 | revs = append(revs, t) 187 | } 188 | } 189 | slices.SortFunc(revs, func(i, j time.Time) int { 190 | if i.Before(j) { 191 | return -1 192 | } 193 | if i.After(j) { 194 | return +1 195 | } 196 | return 0 197 | }) 198 | if len(revs) > 0 { 199 | cve.PublishDate = revs[0] 200 | cve.LastUpdateDate = revs[len(revs)-1] 201 | } 202 | 203 | cves = append(cves, cve) 204 | } 205 | 206 | relations := []MicrosoftKBRelation{} 207 | for _, s := range supercedences { 208 | r := MicrosoftKBRelation{ 209 | KBID: s.KBID, 210 | } 211 | for _, skbid := range s.Supersededby.KBIDs { 212 | r.SupersededBy = append(r.SupersededBy, MicrosoftSupersededBy{ 213 | KBID: skbid, 214 | }) 215 | } 216 | relations = append(relations, r) 217 | } 218 | 219 | return cves, relations 220 | } 221 | -------------------------------------------------------------------------------- /db/redhat.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "iter" 8 | "os" 9 | "strings" 10 | "time" 11 | 12 | "github.com/cheggaaa/pb/v3" 13 | "github.com/spf13/viper" 14 | "golang.org/x/xerrors" 15 | "gorm.io/gorm" 16 | 17 | "github.com/vulsio/gost/models" 18 | "github.com/vulsio/gost/util" 19 | ) 20 | 21 | // GetAfterTimeRedhat : 22 | func (r *RDBDriver) GetAfterTimeRedhat(after time.Time) (allCves []models.RedhatCVE, err error) { 23 | all := []models.RedhatCVE{} 24 | if err = r.conn.Where("public_date >= ?", after.Format("2006-01-02")).Find(&all).Error; err != nil { 25 | return nil, err 26 | } 27 | 28 | // TODO: insufficient 29 | for _, a := range all { 30 | if err = r.conn.Model(&a).Association("Cvss3").Find(&a.Cvss3); err != nil { 31 | return nil, err 32 | } 33 | if err = r.conn.Model(&a).Association("Details").Find(&a.Details); err != nil { 34 | return nil, err 35 | } 36 | if err = r.conn.Model(&a).Association("PackageState").Find(&a.PackageState); err != nil { 37 | return nil, err 38 | } 39 | allCves = append(allCves, a) 40 | } 41 | return allCves, nil 42 | } 43 | 44 | // GetRedhat : 45 | func (r *RDBDriver) GetRedhat(cveID string) (*models.RedhatCVE, error) { 46 | c := models.RedhatCVE{} 47 | if err := r.conn.Where(&models.RedhatCVE{Name: cveID}).First(&c).Error; err != nil { 48 | if errors.Is(err, gorm.ErrRecordNotFound) { 49 | return nil, nil 50 | } 51 | return nil, xerrors.Errorf("Failed to get Redhat. err: %w", err) 52 | } 53 | 54 | if err := r.conn.Model(&c).Association("Details").Find(&c.Details); err != nil { 55 | return nil, xerrors.Errorf("Failed to get Redhat.Details. err: %w", err) 56 | } 57 | if err := r.conn.Model(&c).Association("References").Find(&c.References); err != nil { 58 | return nil, xerrors.Errorf("Failed to get Redhat.References. err: %w", err) 59 | } 60 | if err := r.conn.Model(&c).Association("Bugzilla").Find(&c.Bugzilla); err != nil { 61 | return nil, xerrors.Errorf("Failed to get Redhat.Bugzilla. err: %w", err) 62 | } 63 | if err := r.conn.Model(&c).Association("Cvss").Find(&c.Cvss); err != nil { 64 | return nil, xerrors.Errorf("Failed to get Redhat.Cvss. err: %w", err) 65 | } 66 | if err := r.conn.Model(&c).Association("Cvss3").Find(&c.Cvss3); err != nil { 67 | return nil, xerrors.Errorf("Failed to get Redhat.Cvss3. err: %w", err) 68 | } 69 | if err := r.conn.Model(&c).Association("AffectedRelease").Find(&c.AffectedRelease); err != nil { 70 | return nil, xerrors.Errorf("Failed to get Redhat.AffectedRelease. err: %w", err) 71 | } 72 | if err := r.conn.Model(&c).Association("PackageState").Find(&c.PackageState); err != nil { 73 | return nil, xerrors.Errorf("Failed to get Redhat.PackageState. err: %w", err) 74 | } 75 | return &c, nil 76 | } 77 | 78 | // GetRedhatMulti : 79 | func (r *RDBDriver) GetRedhatMulti(cveIDs []string) (map[string]models.RedhatCVE, error) { 80 | m := map[string]models.RedhatCVE{} 81 | for _, cveID := range cveIDs { 82 | cve, err := r.GetRedhat(cveID) 83 | if err != nil { 84 | return nil, err 85 | } 86 | if cve != nil { 87 | m[cveID] = *cve 88 | } 89 | } 90 | return m, nil 91 | } 92 | 93 | // GetUnfixedCvesRedhat gets the unfixed CVEs. 94 | func (r *RDBDriver) GetUnfixedCvesRedhat(version, pkgName string, ignoreWillNotFix bool) (map[string]models.RedhatCVE, error) { 95 | m := map[string]models.RedhatCVE{} 96 | var cpe string 97 | if strings.HasSuffix(version, "-eus") { 98 | cpe = fmt.Sprintf("cpe:/o:redhat:rhel_eus:%s", strings.TrimSuffix(version, "-eus")) 99 | } else if strings.HasSuffix(version, "-aus") { 100 | cpe = fmt.Sprintf("cpe:/o:redhat:rhel_aus:%s", strings.TrimSuffix(version, "-aus")) 101 | } else { 102 | cpe = fmt.Sprintf("cpe:/o:redhat:enterprise_linux:%s", util.Major(version)) 103 | } 104 | 105 | pkgStats := []models.RedhatPackageState{} 106 | 107 | // https://access.redhat.com/documentation/en-us/red_hat_security_data_api/0.1/html-single/red_hat_security_data_api/index#cve_format 108 | err := r.conn. 109 | Not(map[string]interface{}{"fix_state": []string{"Not affected", "New"}}). 110 | Where(&models.RedhatPackageState{ 111 | Cpe: cpe, 112 | PackageName: pkgName, 113 | }).Find(&pkgStats).Error 114 | if err != nil { 115 | return nil, xerrors.Errorf("Failed to get unfixed cves of Redhat. err: %w", err) 116 | } 117 | 118 | redhatCVEIDs := map[int64]bool{} 119 | for _, p := range pkgStats { 120 | redhatCVEIDs[p.RedhatCVEID] = true 121 | } 122 | 123 | for id := range redhatCVEIDs { 124 | rhcve := models.RedhatCVE{} 125 | if err = r.conn. 126 | Preload("Bugzilla"). 127 | Preload("Cvss"). 128 | Preload("Cvss3"). 129 | Preload("AffectedRelease"). 130 | Preload("PackageState"). 131 | Preload("Details"). 132 | Preload("References"). 133 | Where(&models.RedhatCVE{ID: id}).First(&rhcve).Error; err != nil { 134 | if errors.Is(err, gorm.ErrRecordNotFound) { 135 | return nil, xerrors.Errorf("Failed to get RedhatCVE. DB relationship may be broken, use `$ gost fetch redhat` to recreate DB. err: %w", err) 136 | } 137 | return nil, xerrors.Errorf("Failed to get unfixed cves of Redhat. err: %w", err) 138 | } 139 | 140 | pkgStats := []models.RedhatPackageState{} 141 | for _, pkgstat := range rhcve.PackageState { 142 | if pkgstat.Cpe != cpe || 143 | pkgstat.PackageName != pkgName || 144 | pkgstat.FixState == "Not affected" || 145 | pkgstat.FixState == "New" { 146 | continue 147 | 148 | } else if ignoreWillNotFix && pkgstat.FixState == "Will not fix" { 149 | continue 150 | } 151 | pkgStats = append(pkgStats, pkgstat) 152 | } 153 | if len(pkgStats) == 0 { 154 | continue 155 | } 156 | rhcve.PackageState = pkgStats 157 | m[rhcve.Name] = rhcve 158 | } 159 | return m, nil 160 | } 161 | 162 | // GetAdvisoriesRedHat gets AdvisoryID: []CVE IDs 163 | func (r *RDBDriver) GetAdvisoriesRedHat() (map[string][]string, error) { 164 | m := map[string][]string{} 165 | var cs []models.RedhatCVE 166 | // the maximum value of a host parameter number is SQLITE_MAX_VARIABLE_NUMBER, which defaults to 999 for SQLite versions prior to 3.32.0 (2020-05-22) or 32766 for SQLite versions after 3.32.0. 167 | // https://www.sqlite.org/limits.html Maximum Number Of Host Parameters In A Single SQL Statement 168 | if err := r.conn.Preload("AffectedRelease").FindInBatches(&cs, 999, func(_ *gorm.DB, _ int) error { 169 | for _, c := range cs { 170 | for _, r := range c.AffectedRelease { 171 | m[r.Advisory] = append(m[r.Advisory], c.Name) 172 | } 173 | } 174 | return nil 175 | }).Error; err != nil { 176 | return nil, xerrors.Errorf("Failed to get Redhat. err: %w", err) 177 | } 178 | 179 | for k := range m { 180 | m[k] = util.Unique(m[k]) 181 | } 182 | 183 | return m, nil 184 | } 185 | 186 | // InsertRedhat : 187 | func (r *RDBDriver) InsertRedhat(cves iter.Seq2[models.RedhatCVE, error]) (err error) { 188 | if err := r.deleteAndInsertRedhat(cves); err != nil { 189 | return xerrors.Errorf("Failed to insert RedHat CVE data. err: %w", err) 190 | } 191 | 192 | return nil 193 | } 194 | 195 | func (r *RDBDriver) deleteAndInsertRedhat(cves iter.Seq2[models.RedhatCVE, error]) (err error) { 196 | bar := pb.ProgressBarTemplate(`{{cycle . "[ ]" "[=> ]" "[===> ]" "[=====> ]" "[======> ]" "[========> ]" "[==========> ]" "[============> ]" "[==============> ]" "[================> ]" "[==================> ]" "[===================>]"}} {{counters .}} files processed. ({{speed .}})`).New(0).Start().SetWriter(func() io.Writer { 197 | if viper.GetBool("log-json") { 198 | return io.Discard 199 | } 200 | return os.Stderr 201 | }()) 202 | tx := r.conn.Begin() 203 | 204 | defer func() { 205 | if err != nil { 206 | tx.Rollback() 207 | return 208 | } 209 | tx.Commit() 210 | }() 211 | 212 | // Delete all old records 213 | for _, table := range []interface{}{models.RedhatDetail{}, models.RedhatReference{}, models.RedhatBugzilla{}, models.RedhatCvss{}, models.RedhatCvss3{}, models.RedhatAffectedRelease{}, models.RedhatPackageState{}, models.RedhatCVE{}} { 214 | if err := tx.Session(&gorm.Session{AllowGlobalUpdate: true}).Delete(table).Error; err != nil { 215 | return xerrors.Errorf("Failed to delete old records. err: %w", err) 216 | } 217 | } 218 | 219 | batchSize := viper.GetInt("batch-size") 220 | if batchSize < 1 { 221 | return fmt.Errorf("Failed to set batch-size. err: batch-size option is not set properly") 222 | } 223 | 224 | for chunk, err := range util.Chunk(cves, batchSize) { 225 | if err != nil { 226 | return xerrors.Errorf("Failed to chunk RedHat CVE data. err: %w", err) 227 | } 228 | 229 | if err = tx.Create(chunk).Error; err != nil { 230 | return xerrors.Errorf("Failed to insert. err: %w", err) 231 | } 232 | bar.Add(len(chunk)) 233 | } 234 | bar.Finish() 235 | 236 | return nil 237 | } 238 | -------------------------------------------------------------------------------- /fetcher/redhat.go: -------------------------------------------------------------------------------- 1 | package fetcher 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "iter" 9 | "os" 10 | "path/filepath" 11 | 12 | "golang.org/x/xerrors" 13 | 14 | "github.com/vulsio/gost/models" 15 | "github.com/vulsio/gost/util" 16 | ) 17 | 18 | const ( 19 | redhatRepoURL = "https://github.com/aquasecurity/vuln-list-redhat/archive/refs/heads/main.tar.gz" 20 | redhatDir = "api" 21 | ) 22 | 23 | // FetchRedHatVulnList clones vuln-list and returns CVE JSONs 24 | func FetchRedHatVulnList() (iter.Seq2[models.RedhatCVEJSON, error], error) { 25 | if err := fetchGitArchive(redhatRepoURL, filepath.Join(util.CacheDir(), "vuln-list-redhat"), fmt.Sprintf("vuln-list-redhat-main/%s", redhatDir)); err != nil { 26 | return nil, xerrors.Errorf("Failed to fetch vuln-list-redhat: %w", err) 27 | } 28 | 29 | return func(yield func(models.RedhatCVEJSON, error) bool) { 30 | yieldErr := errors.New("yield error") 31 | if err := filepath.WalkDir(filepath.Join(util.CacheDir(), "vuln-list-redhat"), func(path string, d os.DirEntry, err error) error { 32 | if err != nil { 33 | return err 34 | } 35 | 36 | if d.IsDir() { 37 | return nil 38 | } 39 | 40 | f, err := os.Open(path) 41 | if err != nil { 42 | return xerrors.Errorf("Failed to open file: %w", err) 43 | } 44 | defer f.Close() 45 | 46 | content, err := io.ReadAll(f) 47 | if err != nil { 48 | return err 49 | } 50 | 51 | cve := RedhatCVE{} 52 | if err = json.Unmarshal(content, &cve); err != nil { 53 | return xerrors.Errorf("failed to decode RedHat JSON: %w", err) 54 | } 55 | switch cve.TempAffectedRelease.(type) { 56 | case []interface{}: 57 | var ar RedhatCVEAffectedReleaseArray 58 | if err = json.Unmarshal(content, &ar); err != nil { 59 | return xerrors.Errorf("unknown affected_release type: %w", err) 60 | } 61 | cve.AffectedRelease = ar.AffectedRelease 62 | case map[string]interface{}: 63 | var ar RedhatCVEAffectedReleaseObject 64 | if err = json.Unmarshal(content, &ar); err != nil { 65 | return xerrors.Errorf("unknown affected_release type: %w", err) 66 | } 67 | cve.AffectedRelease = []RedhatAffectedRelease{ar.AffectedRelease} 68 | case nil: 69 | default: 70 | return xerrors.New("unknown affected_release type") 71 | } 72 | 73 | switch cve.TempPackageState.(type) { 74 | case []interface{}: 75 | var ps RedhatCVEPackageStateArray 76 | if err = json.Unmarshal(content, &ps); err != nil { 77 | return xerrors.Errorf("unknown package_state type: %w", err) 78 | } 79 | cve.PackageState = ps.PackageState 80 | case map[string]interface{}: 81 | var ps RedhatCVEPackageStateObject 82 | if err = json.Unmarshal(content, &ps); err != nil { 83 | return xerrors.Errorf("unknown package_state type: %w", err) 84 | } 85 | cve.PackageState = []RedhatPackageState{ps.PackageState} 86 | case nil: 87 | default: 88 | return xerrors.New("unknown package_state type") 89 | } 90 | 91 | if !yield(models.RedhatCVEJSON{ 92 | ThreatSeverity: cve.ThreatSeverity, 93 | PublicDate: cve.PublicDate, 94 | Bugzilla: models.RedhatBugzilla{ 95 | Description: cve.Bugzilla.Description, 96 | BugzillaID: cve.Bugzilla.BugzillaID, 97 | URL: cve.Bugzilla.URL, 98 | }, 99 | Cvss: models.RedhatCvss{ 100 | CvssBaseScore: cve.Cvss.CvssBaseScore, 101 | CvssScoringVector: cve.Cvss.CvssScoringVector, 102 | Status: cve.Cvss.Status, 103 | }, 104 | Cvss3: models.RedhatCvss3{ 105 | Cvss3BaseScore: cve.Cvss3.Cvss3BaseScore, 106 | Cvss3ScoringVector: cve.Cvss3.Cvss3ScoringVector, 107 | Status: cve.Cvss3.Status, 108 | }, 109 | Iava: cve.Iava, 110 | Cwe: cve.Cwe, 111 | Statement: cve.Statement, 112 | Acknowledgement: cve.Acknowledgement, 113 | Mitigation: cve.Mitigation, 114 | TempAffectedRelease: cve.TempAffectedRelease, 115 | AffectedRelease: func() []models.RedhatAffectedRelease { 116 | releases := make([]models.RedhatAffectedRelease, 0, len(cve.AffectedRelease)) 117 | for _, affected := range cve.AffectedRelease { 118 | releases = append(releases, models.RedhatAffectedRelease{ 119 | ProductName: affected.ProductName, 120 | ReleaseDate: affected.ReleaseDate, 121 | Advisory: affected.Advisory, 122 | Package: affected.Package, 123 | Cpe: affected.Cpe, 124 | }) 125 | } 126 | return releases 127 | }(), 128 | PackageState: func() []models.RedhatPackageState { 129 | states := make([]models.RedhatPackageState, 0, len(cve.PackageState)) 130 | for _, state := range cve.PackageState { 131 | states = append(states, models.RedhatPackageState{ 132 | ProductName: state.ProductName, 133 | FixState: state.FixState, 134 | PackageName: state.PackageName, 135 | Cpe: state.Cpe, 136 | }) 137 | } 138 | return states 139 | }(), 140 | Name: cve.Name, 141 | DocumentDistribution: cve.DocumentDistribution, 142 | Details: cve.Details, 143 | References: cve.References, 144 | }, nil) { 145 | return yieldErr 146 | } 147 | 148 | return nil 149 | }); err != nil { 150 | if errors.Is(err, yieldErr) { // No need to call yield with error 151 | return 152 | } 153 | if !yield(models.RedhatCVEJSON{}, xerrors.Errorf("Failed to walk %s: %w", filepath.Join(util.CacheDir(), "vuln-list-redhat"), err)) { 154 | return 155 | } 156 | } 157 | }, nil 158 | } 159 | 160 | // RedhatCVE : 161 | type RedhatCVE struct { 162 | ThreatSeverity string `json:"threat_severity"` 163 | PublicDate string `json:"public_date"` 164 | Bugzilla RedhatBugzilla `json:"bugzilla"` 165 | Cvss RedhatCvss `json:"cvss"` 166 | Cvss3 RedhatCvss3 `json:"cvss3"` 167 | Iava string `json:"iava"` 168 | Cwe string `json:"cwe"` 169 | Statement string `json:"statement"` 170 | Acknowledgement string `json:"acknowledgement"` 171 | Mitigation string `json:"mitigation"` 172 | TempAffectedRelease interface{} `json:"affected_release"` // affected_release is array or object 173 | AffectedRelease []RedhatAffectedRelease 174 | TempPackageState interface{} `json:"package_state"` // package_state is array or object 175 | PackageState []RedhatPackageState 176 | Name string `json:"name"` 177 | DocumentDistribution string `json:"document_distribution"` 178 | 179 | Details []string `json:"details"` 180 | References []string `json:"references"` 181 | } 182 | 183 | // RedhatCVEAffectedReleaseArray : 184 | type RedhatCVEAffectedReleaseArray struct { 185 | AffectedRelease []RedhatAffectedRelease `json:"affected_release"` 186 | } 187 | 188 | // RedhatCVEAffectedReleaseObject : 189 | type RedhatCVEAffectedReleaseObject struct { 190 | AffectedRelease RedhatAffectedRelease `json:"affected_release"` 191 | } 192 | 193 | // RedhatCVEPackageStateArray : 194 | type RedhatCVEPackageStateArray struct { 195 | PackageState []RedhatPackageState `json:"package_state"` 196 | } 197 | 198 | // RedhatCVEPackageStateObject : 199 | type RedhatCVEPackageStateObject struct { 200 | PackageState RedhatPackageState `json:"package_state"` 201 | } 202 | 203 | // RedhatDetail : 204 | type RedhatDetail struct { 205 | Detail string `sql:"type:text"` 206 | } 207 | 208 | // RedhatReference : 209 | type RedhatReference struct { 210 | Reference string `sql:"type:text"` 211 | } 212 | 213 | // RedhatBugzilla : 214 | type RedhatBugzilla struct { 215 | Description string `json:"description" sql:"type:text"` 216 | BugzillaID string `json:"id"` 217 | URL string `json:"url"` 218 | } 219 | 220 | // RedhatCvss : 221 | type RedhatCvss struct { 222 | CvssBaseScore string `json:"cvss_base_score"` 223 | CvssScoringVector string `json:"cvss_scoring_vector"` 224 | Status string `json:"status"` 225 | } 226 | 227 | // RedhatCvss3 : 228 | type RedhatCvss3 struct { 229 | Cvss3BaseScore string `json:"cvss3_base_score"` 230 | Cvss3ScoringVector string `json:"cvss3_scoring_vector"` 231 | Status string `json:"status"` 232 | } 233 | 234 | // RedhatAffectedRelease : 235 | type RedhatAffectedRelease struct { 236 | ProductName string `json:"product_name"` 237 | ReleaseDate string `json:"release_date"` 238 | Advisory string `json:"advisory"` 239 | Package string `json:"package"` 240 | Cpe string `json:"cpe"` 241 | } 242 | 243 | // RedhatPackageState : 244 | type RedhatPackageState struct { 245 | ProductName string `json:"product_name"` 246 | FixState string `json:"fix_state"` 247 | PackageName string `json:"package_name"` 248 | Cpe string `json:"cpe"` 249 | } 250 | -------------------------------------------------------------------------------- /models/redhat.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "iter" 5 | "strings" 6 | "time" 7 | 8 | "github.com/vulsio/gost/util" 9 | ) 10 | 11 | // RedhatEntry : 12 | type RedhatEntry struct { 13 | CveID string `json:"CVE"` 14 | Severity string `json:"severity"` 15 | PublicDate time.Time `json:"public_date"` 16 | Advisories []interface{} `json:"advisories"` 17 | Bugzilla string `json:"bugzilla"` 18 | CvssScore interface{} `json:"cvss_score"` 19 | CvssScoringVector interface{} `json:"cvss_scoring_vector"` 20 | CWE string `json:"CWE"` 21 | AffectedPackages []interface{} `json:"affected_packages"` 22 | ResourceURL string `json:"resource_url"` 23 | Cvss3Score string `json:"cvss3_score"` 24 | Cvss3ScoringVector string `json:"cvss3_scoring_vector"` 25 | } 26 | 27 | // RedhatCVEJSON : 28 | type RedhatCVEJSON struct { 29 | ThreatSeverity string `json:"threat_severity"` 30 | PublicDate string `json:"public_date"` 31 | Bugzilla RedhatBugzilla `json:"bugzilla"` 32 | Cvss RedhatCvss `json:"cvss"` 33 | Cvss3 RedhatCvss3 `json:"cvss3"` 34 | Iava string `json:"iava"` 35 | Cwe string `json:"cwe"` 36 | Statement string `json:"statement"` 37 | Acknowledgement string `json:"acknowledgement"` 38 | TempMitigation interface{} `json:"mitigation"` 39 | Mitigation string 40 | TempAffectedRelease interface{} `json:"affected_release"` // affected_release is array or object 41 | AffectedRelease []RedhatAffectedRelease 42 | TempPackageState interface{} `json:"package_state"` // package_state is array or object 43 | PackageState []RedhatPackageState 44 | Name string `json:"name"` 45 | DocumentDistribution string `json:"document_distribution"` 46 | 47 | Details []string `json:"details" gorm:"-"` 48 | References []string `json:"references" gorm:"-"` 49 | } 50 | 51 | // RedhatCVEJSONAffectedReleaseArray : 52 | type RedhatCVEJSONAffectedReleaseArray struct { 53 | AffectedRelease []RedhatAffectedRelease `json:"affected_release"` 54 | } 55 | 56 | // RedhatCVEJSONAffectedReleaseObject : 57 | type RedhatCVEJSONAffectedReleaseObject struct { 58 | AffectedRelease RedhatAffectedRelease `json:"affected_release"` 59 | } 60 | 61 | // RedhatCVEJSONPackageStateArray : 62 | type RedhatCVEJSONPackageStateArray struct { 63 | PackageState []RedhatPackageState `json:"package_state"` 64 | } 65 | 66 | // RedhatCVEJSONPackageStateObject : 67 | type RedhatCVEJSONPackageStateObject struct { 68 | PackageState RedhatPackageState `json:"package_state"` 69 | } 70 | 71 | // RedhatCVEJSONMitigationObject : 72 | type RedhatCVEJSONMitigationObject struct { 73 | Value string `json:"value"` 74 | Lang string `json:"lang"` 75 | } 76 | 77 | // RedhatCVE : 78 | type RedhatCVE struct { 79 | ID int64 `json:"-"` 80 | 81 | // gorm can't handle embedded struct 82 | ThreatSeverity string `gorm:"type:varchar(255)"` 83 | PublicDate time.Time 84 | Bugzilla RedhatBugzilla 85 | Cvss RedhatCvss 86 | Cvss3 RedhatCvss3 87 | Iava string `gorm:"type:varchar(255)"` 88 | Cwe string `gorm:"type:varchar(255)"` 89 | Statement string `gorm:"type:text"` 90 | Acknowledgement string `gorm:"type:text"` 91 | Mitigation string `gorm:"type:text"` 92 | AffectedRelease []RedhatAffectedRelease 93 | PackageState []RedhatPackageState 94 | Name string `gorm:"type:varchar(255);index:idx_redhat_cves_name"` 95 | DocumentDistribution string `gorm:"type:text"` 96 | 97 | Details []RedhatDetail 98 | References []RedhatReference 99 | } 100 | 101 | // GetDetail returns details 102 | func (r RedhatCVE) GetDetail(sep string) string { 103 | details := []string{} 104 | for _, d := range r.Details { 105 | details = append(details, d.Detail) 106 | } 107 | return strings.Join(details, sep) 108 | } 109 | 110 | // GetPackages returns package names 111 | func (r RedhatCVE) GetPackages(sep string) (result string) { 112 | pkgs := map[string]struct{}{} 113 | for _, d := range r.PackageState { 114 | pkgs[d.PackageName] = struct{}{} 115 | } 116 | 117 | pkgNames := []string{} 118 | for p := range pkgs { 119 | pkgNames = append(pkgNames, p) 120 | } 121 | 122 | return strings.Join(pkgNames, sep) 123 | } 124 | 125 | // RedhatDetail : 126 | type RedhatDetail struct { 127 | ID int64 `json:"-"` 128 | RedhatCVEID int64 `json:"-" gorm:"index:idx_redhat_details_redhat_cve_id"` 129 | Detail string `gorm:"type:text"` 130 | } 131 | 132 | // RedhatReference : 133 | type RedhatReference struct { 134 | ID int64 `json:"-"` 135 | RedhatCVEID int64 `json:"-" gorm:"index:idx_redhat_references_redhat_cve_id"` 136 | Reference string `gorm:"type:text"` 137 | } 138 | 139 | // RedhatBugzilla : 140 | type RedhatBugzilla struct { 141 | ID int64 `json:"-"` 142 | RedhatCVEID int64 `json:"-" gorm:"index:idx_redhat_bugzillas_redhat_cve_id"` 143 | Description string `json:"description" gorm:"type:text"` 144 | 145 | BugzillaID string `json:"id" gorm:"type:varchar(255)"` 146 | URL string `json:"url" gorm:"type:varchar(255)"` 147 | } 148 | 149 | // RedhatCvss : 150 | type RedhatCvss struct { 151 | ID int64 `json:"-"` 152 | RedhatCVEID int64 `json:"-" gorm:"index:idx_redhat_cvsses_redhat_cve_id"` 153 | CvssBaseScore string `json:"cvss_base_score" gorm:"type:varchar(255)"` 154 | CvssScoringVector string `json:"cvss_scoring_vector" gorm:"type:varchar(255)"` 155 | Status string `json:"status" gorm:"type:varchar(255)"` 156 | } 157 | 158 | // RedhatCvss3 : 159 | type RedhatCvss3 struct { 160 | ID int64 `json:"-"` 161 | RedhatCVEID int64 `json:"-" gorm:"index:idx_redhat_cvss3_redhat_cve_id"` 162 | Cvss3BaseScore string `json:"cvss3_base_score" gorm:"type:varchar(255)"` 163 | Cvss3ScoringVector string `json:"cvss3_scoring_vector" gorm:"type:varchar(255)"` 164 | Status string `json:"status" gorm:"type:varchar(255)"` 165 | } 166 | 167 | // RedhatAffectedRelease : 168 | type RedhatAffectedRelease struct { 169 | ID int64 `json:"-"` 170 | RedhatCVEID int64 `json:"-" gorm:"index:idx_redhat_affected_releases_redhat_cve_id"` 171 | ProductName string `json:"product_name" gorm:"type:varchar(255)"` 172 | ReleaseDate string `json:"release_date" gorm:"type:varchar(255)"` 173 | Advisory string `json:"advisory" gorm:"type:varchar(255)"` 174 | Package string `json:"package" gorm:"type:varchar(255)"` 175 | Cpe string `json:"cpe" gorm:"type:varchar(255)"` 176 | } 177 | 178 | // RedhatPackageState : 179 | type RedhatPackageState struct { 180 | ID int64 `json:"-"` 181 | RedhatCVEID int64 `json:"-" gorm:"index:idx_redhat_package_states_redhat_cve_id"` 182 | ProductName string `json:"product_name" gorm:"type:varchar(255)"` 183 | FixState string `json:"fix_state" gorm:"type:varchar(255);index:idx_redhat_package_states_fix_state"` 184 | PackageName string `json:"package_name" gorm:"type:varchar(255);index:idx_redhat_package_states_package_name"` 185 | Cpe string `json:"cpe" gorm:"type:varchar(255);index:idx_redhat_package_states_cpe"` 186 | } 187 | 188 | // ConvertRedhat : 189 | func ConvertRedhat(cveJSONs iter.Seq2[RedhatCVEJSON, error]) iter.Seq2[RedhatCVE, error] { 190 | return func(yield func(RedhatCVE, error) bool) { 191 | for cve, err := range cveJSONs { 192 | if err != nil { 193 | if !yield(RedhatCVE{}, err) { 194 | return 195 | } 196 | continue 197 | } 198 | 199 | if !yield(RedhatCVE{ 200 | ThreatSeverity: cve.ThreatSeverity, 201 | PublicDate: func() time.Time { 202 | if cve.PublicDate != "" { 203 | for _, layout := range []string{"2006-01-02T15:04:05Z", "2006-01-02T15:04:05"} { 204 | t, err := time.Parse(layout, cve.PublicDate) 205 | if err == nil { 206 | return t 207 | } 208 | } 209 | } 210 | return time.Date(1000, time.January, 1, 0, 0, 0, 0, time.UTC) 211 | }(), 212 | Bugzilla: RedhatBugzilla{ 213 | Description: util.TrimSpaceNewline(cve.Bugzilla.Description), 214 | BugzillaID: cve.Bugzilla.BugzillaID, 215 | URL: cve.Bugzilla.URL, 216 | }, 217 | Cvss: cve.Cvss, 218 | Cvss3: cve.Cvss3, 219 | Iava: cve.Iava, 220 | Cwe: cve.Cwe, 221 | Statement: util.TrimSpaceNewline(cve.Statement), 222 | Acknowledgement: cve.Acknowledgement, 223 | Mitigation: cve.Mitigation, 224 | AffectedRelease: cve.AffectedRelease, 225 | PackageState: cve.PackageState, 226 | Name: cve.Name, 227 | DocumentDistribution: cve.DocumentDistribution, 228 | 229 | Details: func() []RedhatDetail { 230 | details := make([]RedhatDetail, 0, len(cve.Details)) 231 | for _, d := range cve.Details { 232 | details = append(details, RedhatDetail{Detail: util.TrimSpaceNewline(d)}) 233 | } 234 | return details 235 | }(), 236 | References: func() []RedhatReference { 237 | var rs []RedhatReference 238 | for _, r := range cve.References { 239 | for l := range strings.Lines(r) { 240 | rs = append(rs, RedhatReference{Reference: util.TrimSpaceNewline(l)}) 241 | } 242 | } 243 | return rs 244 | }(), 245 | }, nil) { 246 | return 247 | } 248 | } 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /fetcher/debian.go: -------------------------------------------------------------------------------- 1 | package fetcher 2 | 3 | import ( 4 | "archive/tar" 5 | "bytes" 6 | "compress/gzip" 7 | "context" 8 | "encoding/json" 9 | "io" 10 | "net/http" 11 | "os" 12 | "path/filepath" 13 | "strings" 14 | 15 | "github.com/aquasecurity/trivy-db/pkg/types" 16 | ocispec "github.com/opencontainers/image-spec/specs-go/v1" 17 | bolt "go.etcd.io/bbolt" 18 | "golang.org/x/xerrors" 19 | "oras.land/oras-go/v2/content" 20 | "oras.land/oras-go/v2/registry/remote" 21 | 22 | "github.com/vulsio/gost/models" 23 | "github.com/vulsio/gost/util" 24 | ) 25 | 26 | // RetrieveDebianCveDetails returns CVE details 27 | func RetrieveDebianCveDetails() (models.DebianJSON, error) { 28 | cves, err := retrieveDebianSecurityTrackerAPI() 29 | if err != nil { 30 | return nil, xerrors.Errorf("Failed to fetch CVE details from Debian Security Tracker API. err: %w", err) 31 | } 32 | 33 | cvesTrivyDB, err := retrieveTrivyDB() 34 | if err != nil { 35 | return nil, xerrors.Errorf("Failed to fetch CVE details from Trivy-DB. err: %w", err) 36 | } 37 | 38 | for pkg, cvemap := range cvesTrivyDB { 39 | if cves[pkg] == nil { 40 | cves[pkg] = models.DebianCveMap{} 41 | } 42 | for cve, e := range cvemap { 43 | cm := cves[pkg][cve] 44 | if cm.Description == "" { 45 | cm.Description = e.Description 46 | } 47 | if cm.Releases == nil { 48 | cm.Releases = map[string]models.DebianReleaseJSON{} 49 | } 50 | for codename, r := range e.Releases { 51 | if _, ok := cm.Releases[codename]; !ok { 52 | cm.Releases[codename] = r 53 | } 54 | } 55 | cves[pkg][cve] = cm 56 | } 57 | } 58 | 59 | return cves, nil 60 | } 61 | 62 | func retrieveDebianSecurityTrackerAPI() (models.DebianJSON, error) { 63 | resp, err := util.FetchURL("https://security-tracker.debian.org/tracker/data/json") 64 | if err != nil { 65 | return nil, xerrors.Errorf("Failed to fetch cve data from Debian Security Tracker API. err: %w", err) 66 | } 67 | defer resp.Body.Close() 68 | 69 | if resp.StatusCode != http.StatusOK { 70 | return nil, xerrors.Errorf("Failed to fetch cve data from Debian Security Tracker API. err: status code: %d", resp.StatusCode) 71 | } 72 | 73 | var cves models.DebianJSON 74 | if err := json.NewDecoder(resp.Body).Decode(&cves); err != nil { 75 | return nil, xerrors.Errorf("Failed to decode Debian JSON. err: %w", err) 76 | } 77 | 78 | return cves, nil 79 | } 80 | 81 | func retrieveTrivyDB() (models.DebianJSON, error) { 82 | ctx := context.Background() 83 | 84 | // $ oras manifest fetch --media-type "application/vnd.oci.image.manifest.v1+json" ghcr.io/aquasecurity/trivy-db:latest 85 | // {"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","config":{"mediaType":"application/vnd.aquasec.trivy.config.v1+json","digest":"sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","size":2},"layers":[{"mediaType":"application/vnd.aquasec.trivy.db.layer.v1.tar+gzip","digest":"sha256:4659ee8e31616ad4cf61d0e71add03bbb39fd61a778b692d29b90b95bbfab0ec","size":39679068,"annotations":{"org.opencontainers.image.title":"db.tar.gz"}}],"annotations":{"org.opencontainers.image.created":"2023-06-22T18:07:53Z"}} 86 | d, err := fetchManifest(ctx) 87 | if err != nil { 88 | return nil, xerrors.Errorf("Failed to fetch manifest. err: %w", err) 89 | } 90 | 91 | // oras blob fetch --output ${cache dir}/db.tar.gz ghcr.io/aquasecurity/trivy-db@sha256:0ecd2dfd4f851f49167b98f3aaf73e67d704d006c58bc3ac41a9f19a9731163 92 | // tar zfx db.tar.gz trivy.db 93 | dbpath, err := fetchBlob(ctx, d) 94 | if err != nil { 95 | return nil, xerrors.Errorf("Failed to fetch blob. err: %w", err) 96 | } 97 | 98 | return walkDB(dbpath) 99 | } 100 | 101 | func fetchManifest(ctx context.Context) (ocispec.Descriptor, error) { 102 | src, err := remote.NewRepository("ghcr.io/aquasecurity/trivy-db") 103 | if err != nil { 104 | return ocispec.Descriptor{}, xerrors.Errorf("Failed to create client to ghcr.io/aquasecurity/trivy-db. err: %w", err) 105 | } 106 | src.ManifestMediaTypes = []string{"application/vnd.oci.image.manifest.v1+json"} 107 | 108 | desc, rc, err := src.FetchReference(ctx, "latest") 109 | if err != nil { 110 | return ocispec.Descriptor{}, xerrors.Errorf("Failed to fetch the manifest identified by the reference. err: %w", err) 111 | } 112 | defer rc.Close() 113 | 114 | bs, err := content.ReadAll(rc, desc) 115 | if err != nil { 116 | return ocispec.Descriptor{}, xerrors.Errorf("Failed to read content. err: %w", err) 117 | } 118 | 119 | var manifest ocispec.Manifest 120 | if err := json.Unmarshal(bs, &manifest); err != nil { 121 | return ocispec.Descriptor{}, xerrors.Errorf("Failed to unmarshal json. err: %w", err) 122 | } 123 | 124 | for _, l := range manifest.Layers { 125 | if l.MediaType == "application/vnd.aquasec.trivy.db.layer.v1.tar+gzip" { 126 | return l, nil 127 | } 128 | } 129 | return ocispec.Descriptor{}, xerrors.Errorf("not found digest and filename from layers, actual layers: %#v", manifest.Layers) 130 | } 131 | 132 | func fetchBlob(ctx context.Context, desc ocispec.Descriptor) (string, error) { 133 | src, err := remote.NewRepository("ghcr.io/aquasecurity/trivy-db") 134 | if err != nil { 135 | return "", xerrors.Errorf("Failed to create client to ghcr.io/aquasecurity/trivy-db. err: %w", err) 136 | } 137 | 138 | rc, err := src.Fetch(ctx, desc) 139 | if err != nil { 140 | return "", xerrors.Errorf("Failed to fetch the manifest identified by the reference. err: %w", err) 141 | } 142 | defer rc.Close() 143 | 144 | bs, err := content.ReadAll(rc, desc) 145 | if err != nil { 146 | return "", xerrors.Errorf("Failed to read content. err: %w", err) 147 | } 148 | 149 | gr, err := gzip.NewReader(bytes.NewReader(bs)) 150 | if err != nil { 151 | return "", xerrors.Errorf("Failed to create new gzip reader. err: %w", err) 152 | } 153 | 154 | tr := tar.NewReader(gr) 155 | 156 | for { 157 | header, err := tr.Next() 158 | if err == io.EOF { 159 | break 160 | } 161 | 162 | if header.Name == "trivy.db" { 163 | p := filepath.Join(util.CacheDir(), "trivy.db") 164 | if err := func(dbpath string) error { 165 | if err := os.MkdirAll(util.CacheDir(), 0700); err != nil { 166 | return xerrors.Errorf("Failed to mkdir %s: %w", util.CacheDir(), err) 167 | } 168 | 169 | f, err := os.Create(dbpath) 170 | if err != nil { 171 | return xerrors.Errorf("Failed to create %s. err: %w", p, err) 172 | } 173 | defer f.Close() 174 | 175 | if _, err := io.Copy(f, tr); err != nil { 176 | return xerrors.Errorf("Failed to copy from src to dst. err: %w", err) 177 | } 178 | 179 | return nil 180 | }(p); err != nil { 181 | return "", xerrors.Errorf("Failed to create trivy.db. err: %w", err) 182 | } 183 | return p, nil 184 | } 185 | } 186 | 187 | return "", xerrors.Errorf("not found trivy.db in ghcr.io/aquasecurity/trivy-db@%s", desc.Digest.String()) 188 | } 189 | 190 | func walkDB(dbpath string) (models.DebianJSON, error) { 191 | debVerCodename := map[string]string{ 192 | "7": "wheezy", 193 | "8": "jessie", 194 | "9": "stretch", 195 | "10": "buster", 196 | "11": "bullseye", 197 | "12": "bookworm", 198 | "13": "trixie", 199 | "14": "forky", 200 | "15": "duke", 201 | } 202 | 203 | cves := models.DebianJSON{} 204 | 205 | db, err := bolt.Open(dbpath, 0600, nil) 206 | if err != nil { 207 | return nil, xerrors.Errorf("Failed to open db. err: %w", err) 208 | } 209 | defer db.Close() 210 | 211 | if err := db.View(func(tx *bolt.Tx) error { 212 | ds := map[string]string{} 213 | if err := tx.Bucket([]byte("vulnerability")).ForEach(func(cve, bs []byte) error { 214 | var v types.Vulnerability 215 | if err := json.Unmarshal(bs, &v); err != nil { 216 | return xerrors.Errorf("Failed to unmarshal json. err: %w", err) 217 | } 218 | ds[string(cve)] = v.Description 219 | 220 | return nil 221 | }); err != nil { 222 | return xerrors.Errorf("Failed to foreach vulnerability. err: %w", err) 223 | } 224 | 225 | if err := tx.ForEach(func(bn []byte, b *bolt.Bucket) error { 226 | s := string(bn) 227 | if !strings.HasPrefix(s, "debian ") { 228 | return nil 229 | } 230 | 231 | codename, ok := debVerCodename[strings.TrimPrefix(s, "debian ")] 232 | if !ok { 233 | return xerrors.Errorf("not found debian major version. actual: %s", strings.TrimPrefix(s, "debian ")) 234 | } 235 | 236 | if err := b.ForEachBucket(func(pkg []byte) error { 237 | if err := b.Bucket(pkg).ForEach(func(cve, bs []byte) error { 238 | var a types.Advisory 239 | if err := json.Unmarshal(bs, &a); err != nil { 240 | return xerrors.Errorf("Failed to unmarshal json. err: %w", err) 241 | } 242 | 243 | if cves[string(pkg)] == nil { 244 | cves[string(pkg)] = models.DebianCveMap{} 245 | } 246 | cm := cves[string(pkg)][string(cve)] 247 | cm.Description = ds[string(cve)] 248 | if cm.Releases == nil { 249 | cm.Releases = map[string]models.DebianReleaseJSON{} 250 | } 251 | 252 | status := "open" 253 | if a.FixedVersion != "" { 254 | status = "resolved" 255 | } 256 | urgency := strings.ToLower(a.Severity.String()) 257 | if urgency == "unknown" && a.State != "" { 258 | urgency = a.State 259 | } 260 | r := models.DebianReleaseJSON{ 261 | Status: status, 262 | FixedVersion: a.FixedVersion, 263 | Urgency: urgency, 264 | } 265 | cm.Releases[codename] = r 266 | 267 | cves[string(pkg)][string(cve)] = cm 268 | 269 | return nil 270 | }); err != nil { 271 | return xerrors.Errorf("Failed to foreach %s. err: %w", string(pkg), err) 272 | } 273 | return nil 274 | }); err != nil { 275 | return xerrors.Errorf("Failed to foreach %s. err: %w", string(bn), err) 276 | } 277 | return nil 278 | }); err != nil { 279 | return xerrors.Errorf("Failed to foreach. err: %w", err) 280 | } 281 | return nil 282 | }); err != nil { 283 | return nil, xerrors.Errorf("Failed to view db. err: %w", err) 284 | } 285 | 286 | return cves, nil 287 | } 288 | -------------------------------------------------------------------------------- /db/ubuntu.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "iter" 8 | "os" 9 | "strings" 10 | 11 | "github.com/cheggaaa/pb/v3" 12 | "github.com/spf13/viper" 13 | "golang.org/x/xerrors" 14 | "gorm.io/gorm" 15 | 16 | "github.com/vulsio/gost/models" 17 | "github.com/vulsio/gost/util" 18 | ) 19 | 20 | // GetUbuntu : 21 | func (r *RDBDriver) GetUbuntu(cveID string) (*models.UbuntuCVE, error) { 22 | c := models.UbuntuCVE{} 23 | if err := r.conn.Where(&models.UbuntuCVE{Candidate: cveID}).First(&c).Error; err != nil { 24 | if errors.Is(err, gorm.ErrRecordNotFound) { 25 | return nil, nil 26 | } 27 | return nil, xerrors.Errorf("Failed to get Ubuntu. err: %w", err) 28 | } 29 | 30 | if err := r.conn.Model(&c).Association("References").Find(&c.References); err != nil { 31 | return nil, xerrors.Errorf("Failed to get Ubuntu.References. err: %w", err) 32 | } 33 | if err := r.conn.Model(&c).Association("Notes").Find(&c.Notes); err != nil { 34 | return nil, xerrors.Errorf("Failed to get Ubuntu.Notes. err: %w", err) 35 | } 36 | if err := r.conn.Model(&c).Association("Bugs").Find(&c.Bugs); err != nil { 37 | return nil, xerrors.Errorf("Failed to get Ubuntu.Bugs. err: %w", err) 38 | } 39 | if err := r.conn.Model(&c).Association("Patches").Find(&c.Patches); err != nil { 40 | return nil, xerrors.Errorf("Failed to get Ubuntu.Patches. err: %w", err) 41 | } 42 | patches := []models.UbuntuPatch{} 43 | for _, p := range c.Patches { 44 | if err := r.conn.Model(&p).Association("ReleasePatches").Find(&p.ReleasePatches); err != nil { 45 | return nil, xerrors.Errorf("Failed to get Ubuntu.ReleasePatches. err: %w", err) 46 | } 47 | patches = append(patches, p) 48 | } 49 | c.Patches = patches 50 | if err := r.conn.Model(&c).Association("Upstreams").Find(&c.Upstreams); err != nil { 51 | return nil, xerrors.Errorf("Failed to get Ubuntu.Upstreams. err: %w", err) 52 | } 53 | upstreams := []models.UbuntuUpstream{} 54 | for _, u := range c.Upstreams { 55 | if err := r.conn.Model(&u).Association("UpstreamLinks").Find(&u.UpstreamLinks); err != nil { 56 | return nil, xerrors.Errorf("Failed to get Ubuntu.UpstreamLinks err: %w", err) 57 | } 58 | upstreams = append(upstreams, u) 59 | } 60 | c.Upstreams = upstreams 61 | 62 | return &c, nil 63 | } 64 | 65 | // GetUbuntuMulti : 66 | func (r *RDBDriver) GetUbuntuMulti(cveIDs []string) (map[string]models.UbuntuCVE, error) { 67 | m := map[string]models.UbuntuCVE{} 68 | for _, cveID := range cveIDs { 69 | cve, err := r.GetUbuntu(cveID) 70 | if err != nil { 71 | return nil, err 72 | } 73 | if cve != nil { 74 | m[cveID] = *cve 75 | } 76 | } 77 | return m, nil 78 | } 79 | 80 | // InsertUbuntu : 81 | func (r *RDBDriver) InsertUbuntu(cves iter.Seq2[models.UbuntuCVE, error]) (err error) { 82 | if err = r.deleteAndInsertUbuntu(cves); err != nil { 83 | return xerrors.Errorf("Failed to insert Ubuntu CVE data. err: %s", err) 84 | } 85 | 86 | return nil 87 | } 88 | 89 | func (r *RDBDriver) deleteAndInsertUbuntu(cves iter.Seq2[models.UbuntuCVE, error]) (err error) { 90 | bar := pb.ProgressBarTemplate(`{{cycle . "[ ]" "[=> ]" "[===> ]" "[=====> ]" "[======> ]" "[========> ]" "[==========> ]" "[============> ]" "[==============> ]" "[================> ]" "[==================> ]" "[===================>]"}} {{counters .}} files processed. ({{speed .}})`).New(0).Start().SetWriter(func() io.Writer { 91 | if viper.GetBool("log-json") { 92 | return io.Discard 93 | } 94 | return os.Stderr 95 | }()) 96 | tx := r.conn.Begin() 97 | 98 | defer func() { 99 | if err != nil { 100 | tx.Rollback() 101 | return 102 | } 103 | tx.Commit() 104 | }() 105 | 106 | // Delete all old records 107 | for _, table := range []interface{}{models.UbuntuUpstreamLink{}, models.UbuntuUpstream{}, models.UbuntuReleasePatch{}, models.UbuntuPatch{}, models.UbuntuBug{}, models.UbuntuNote{}, models.UbuntuReference{}, models.UbuntuCVE{}} { 108 | if err := tx.Session(&gorm.Session{AllowGlobalUpdate: true}).Delete(table).Error; err != nil { 109 | return xerrors.Errorf("Failed to delete old records. err: %w", err) 110 | } 111 | } 112 | 113 | batchSize := viper.GetInt("batch-size") 114 | if batchSize < 1 { 115 | return xerrors.New("Failed to set batch-size. err: batch-size option is not set properly") 116 | } 117 | 118 | for chunk, err := range util.Chunk(cves, batchSize) { 119 | if err != nil { 120 | return xerrors.Errorf("Failed to chunk Ubuntu CVE data. err: %w", err) 121 | } 122 | 123 | if err = tx.Create(chunk).Error; err != nil { 124 | return xerrors.Errorf("Failed to insert. err: %w", err) 125 | } 126 | bar.Add(len(chunk)) 127 | } 128 | bar.Finish() 129 | 130 | return nil 131 | } 132 | 133 | var ubuntuVerCodename = map[string]string{ 134 | "606": "dapper", 135 | "610": "edgy", 136 | "704": "feisty", 137 | "710": "gutsy", 138 | "804": "hardy", 139 | "810": "intrepid", 140 | "904": "jaunty", 141 | "910": "karmic", 142 | "1004": "lucid", 143 | "1010": "maverick", 144 | "1104": "natty", 145 | "1110": "oneiric", 146 | "1204": "precise", 147 | "1210": "quantal", 148 | "1304": "raring", 149 | "1310": "saucy", 150 | "1404": "trusty", 151 | "1410": "utopic", 152 | "1504": "vivid", 153 | "1510": "wily", 154 | "1604": "xenial", 155 | "1610": "yakkety", 156 | "1704": "zesty", 157 | "1710": "artful", 158 | "1804": "bionic", 159 | "1810": "cosmic", 160 | "1904": "disco", 161 | "1910": "eoan", 162 | "2004": "focal", 163 | "2010": "groovy", 164 | "2104": "hirsute", 165 | "2110": "impish", 166 | "2204": "jammy", 167 | "2210": "kinetic", 168 | "2304": "lunar", 169 | "2310": "mantic", 170 | "2404": "noble", 171 | "2410": "oracular", 172 | "2504": "plucky", 173 | } 174 | 175 | // GetUnfixedCvesUbuntu gets the CVEs related to ubuntu_release_patches.status IN ('needed', 'deferred', 'pending'), ver, pkgName. 176 | func (r *RDBDriver) GetUnfixedCvesUbuntu(ver, pkgName string) (map[string]models.UbuntuCVE, error) { 177 | return r.getCvesUbuntuWithFixStatus(ver, pkgName, []string{"needed", "deferred", "pending"}) 178 | } 179 | 180 | // GetFixedCvesUbuntu gets the CVEs related to ubuntu_release_patches.status IN ('released'), ver, pkgName. 181 | func (r *RDBDriver) GetFixedCvesUbuntu(ver, pkgName string) (map[string]models.UbuntuCVE, error) { 182 | return r.getCvesUbuntuWithFixStatus(ver, pkgName, []string{"released"}) 183 | } 184 | 185 | func (r *RDBDriver) getCvesUbuntuWithFixStatus(ver, pkgName string, fixStatus []string) (map[string]models.UbuntuCVE, error) { 186 | codeName, ok := ubuntuVerCodename[ver] 187 | if !ok { 188 | return nil, xerrors.Errorf("Failed to convert from major version to codename. err: Ubuntu %s is not supported yet", ver) 189 | } 190 | esmCodeNames := []string{ 191 | codeName, 192 | fmt.Sprintf("esm-apps/%s", codeName), 193 | fmt.Sprintf("esm-infra/%s", codeName), 194 | fmt.Sprintf("%s/esm", codeName), 195 | fmt.Sprintf("ros-esm/%s", codeName), 196 | } 197 | 198 | type Result struct { 199 | UbuntuCveID int64 200 | } 201 | 202 | results := []Result{} 203 | err := r.conn. 204 | Table("ubuntu_patches"). 205 | Select("ubuntu_cve_id"). 206 | Where("package_name = ?", pkgName). 207 | Scan(&results).Error 208 | 209 | if err != nil { 210 | if fixStatus[0] == "released" { 211 | return nil, xerrors.Errorf("Failed to get fixed cves of Ubuntu. err: %w", err) 212 | } 213 | return nil, xerrors.Errorf("Failed to get unfixed cves of Ubuntu. err: %w", err) 214 | } 215 | 216 | m := map[string]models.UbuntuCVE{} 217 | for _, res := range results { 218 | cve := models.UbuntuCVE{} 219 | if err := r.conn. 220 | Preload("Patches.ReleasePatches", "release_name IN (?) AND status IN (?)", esmCodeNames, fixStatus). 221 | Preload("Patches", "package_name = ?", pkgName). 222 | Where(&models.UbuntuCVE{ID: res.UbuntuCveID}). 223 | First(&cve).Error; err != nil { 224 | if errors.Is(err, gorm.ErrRecordNotFound) { 225 | return nil, xerrors.Errorf("Failed to get UbuntuCVE. DB relationship may be broken, use `$ gost fetch ubuntu` to recreate DB. err: %w", err) 226 | } 227 | return nil, xerrors.Errorf("Failed to get UbuntuCVE. err: %w", err) 228 | } 229 | 230 | if err := r.conn.Model(&cve).Association("References").Find(&cve.References); err != nil { 231 | return nil, err 232 | } 233 | if err := r.conn.Model(&cve).Association("Notes").Find(&cve.Notes); err != nil { 234 | return nil, err 235 | } 236 | if err := r.conn.Model(&cve).Association("Bugs").Find(&cve.Bugs); err != nil { 237 | return nil, err 238 | } 239 | if err := r.conn.Model(&cve).Association("Upstreams").Find(&cve.Upstreams); err != nil { 240 | return nil, err 241 | } 242 | upstreams := []models.UbuntuUpstream{} 243 | for _, u := range cve.Upstreams { 244 | if err := r.conn.Model(&u).Association("UpstreamLinks").Find(&u.UpstreamLinks); err != nil { 245 | return nil, err 246 | } 247 | upstreams = append(upstreams, u) 248 | } 249 | cve.Upstreams = upstreams 250 | 251 | if len(cve.Patches) != 0 { 252 | for _, p := range cve.Patches { 253 | if len(p.ReleasePatches) != 0 { 254 | m[cve.Candidate] = cve 255 | } 256 | } 257 | } 258 | } 259 | 260 | return m, nil 261 | } 262 | 263 | // GetAdvisoriesUbuntu gets AdvisoryID: []CVE IDs 264 | func (r *RDBDriver) GetAdvisoriesUbuntu() (map[string][]string, error) { 265 | m := map[string][]string{} 266 | var cs []models.UbuntuCVE 267 | // the maximum value of a host parameter number is SQLITE_MAX_VARIABLE_NUMBER, which defaults to 999 for SQLite versions prior to 3.32.0 (2020-05-22) or 32766 for SQLite versions after 3.32.0. 268 | // https://www.sqlite.org/limits.html Maximum Number Of Host Parameters In A Single SQL Statement 269 | if err := r.conn.Preload("References", "reference LIKE ?", "https://ubuntu.com/security/notices/USN-%").FindInBatches(&cs, 999, func(_ *gorm.DB, _ int) error { 270 | for _, c := range cs { 271 | for _, r := range c.References { 272 | m[strings.TrimPrefix(r.Reference, "https://ubuntu.com/security/notices/")] = append(m[strings.TrimPrefix(r.Reference, "https://ubuntu.com/security/notices/")], c.Candidate) 273 | } 274 | } 275 | return nil 276 | }).Error; err != nil { 277 | return nil, xerrors.Errorf("Failed to get Ubuntu. err: %w", err) 278 | } 279 | 280 | for k := range m { 281 | m[k] = util.Unique(m[k]) 282 | } 283 | 284 | return m, nil 285 | } 286 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gost (go-security-tracker) 2 | [![MIT License](http://img.shields.io/badge/license-MIT-blue.svg?style=flat)](https://github.com/vulsio/gost/blob/master/LICENSE) 3 | 4 | `gost` builds a local copy of Security Tracker(Redhat/Debian/Ubuntu/Microsoft). 5 | After you register CVEs to watch list, `gost` notify via E-mail/Slack if there is an update. 6 | The pronunciation of `gost` is the same as the English word "ghost". 7 | 8 | 9 | 10 | # Abstract 11 | `gost` is written in Go, and therefore you can just grab the binary releases and drop it in your $PATH. 12 | 13 | `gost` builds a local copy of Security Tracker ([Redhat](https://access.redhat.com/security/security-updates/) or [Debian](https://security-tracker.debian.org/tracker/) or [Ubuntu](https://people.canonical.com/~ubuntu-security/cve/) or [Microsoft](https://portal.msrc.microsoft.com/en-us/security-guidance)). 14 | 15 | A system administrator always monitor `Security Tracker`. It can be a burden. For example, after the vulnerability is found, we have to wait until the patch comes out. I hope anyone notifies me if there is an update. 16 | 17 | # Main features 18 | `gost` has the following features. 19 | - Build a local copy of Security Tracker 20 | - A server mode for easy querying 21 | - Register CVEs to watch list 22 | - Notify if there is an update (E-Mail or Slack) 23 | - Monitoring metric can be specified (e.g. CVSS Score, Severity, etc.) 24 | 25 | # Usage 26 | 27 | ``` 28 | $ gost help 29 | Security Tracker 30 | 31 | Usage: 32 | gost [command] 33 | 34 | Available Commands: 35 | completion generate the autocompletion script for the specified shell 36 | fetch Fetch the data of the security tracker 37 | help Help about any command 38 | notify Notifiy update about the specified CVE 39 | register Register CVEs to monitor 40 | server Start security tracker HTTP server 41 | version Show version 42 | 43 | Flags: 44 | --config string config file (default is $HOME/.gost.yaml) 45 | --dbpath string /path/to/sqlite3 or SQL connection string (default "$PWD/gost.sqlite3") 46 | --dbtype string Database type to store data in (sqlite3, mysql, postgres or redis supported) (default "sqlite3") 47 | --debug debug mode 48 | --debug-sql SQL debug mode 49 | -h, --help help for gost 50 | --http-proxy string http://proxy-url:port (default: empty) 51 | --log-dir string /path/to/log (default "/var/log/gost") 52 | --log-json output log as JSON 53 | --log-to-file output log to file 54 | --to-email Send notification via Email 55 | --to-slack Send notification via Slack 56 | 57 | Use "gost [command] --help" for more information about a command. 58 | ``` 59 | 60 | # Fetch RedHat 61 | 62 | ## Fetch vulnerability infomation updated after 2016-01-01 63 | 64 | ``` 65 | $ gost fetch redhat 66 | 67 | INFO[07-27|11:13:27] Initialize Database 68 | INFO[07-27|11:13:27] Opening DB. db=sqlite3 69 | INFO[07-27|11:13:27] Migrating DB. db=sqlite3 70 | INFO[07-27|11:13:27] Fetch the list of CVEs 71 | INFO[07-27|13:59:33] Fetched 6136 CVEs 72 | 6136 / 6136 [=================] 100.00% 8m25s 73 | INFO[07-27|14:08:00] Insert RedHat into DB db=sqlite3 74 | 0 / 6136 [--------------------] 0.00%INFO[07-27|14:08:00] Insert 6136 CVEs 75 | 6136 / 6136 [=================] 100.00% 17s 76 | ``` 77 | 78 | # Fetch Debian 79 | 80 | ## Fetch vulnerability infomation 81 | 82 | ``` 83 | $ gost fetch debian 84 | 85 | INFO[07-27|15:30:49] Initialize Database 86 | INFO[07-27|15:30:49] Opening DB. db=sqlite3 87 | INFO[07-27|15:30:49] Migrating DB. db=sqlite3 88 | INFO[07-27|15:30:49] Fetched all CVEs from Debian 89 | INFO[07-27|15:31:09] Insert Debian CVEs into DB db=sqlite3 90 | 21428 / 21428 [================] 100.00% 5s 91 | ``` 92 | 93 | # Fetch Ubuntu 94 | 95 | ## Fetch vulnerability infomation 96 | 97 | ``` 98 | $ gost fetch ubuntu 99 | 100 | INFO[05-23|06:28:18] Initialize Database 101 | INFO[05-23|06:28:18] Fetched CVEs=36737 102 | INFO[05-23|06:28:18] Insert Ubuntu into DB db=sqlite3 103 | 36737 / 36737 [============================================================================] 100.00% 55s 104 | ``` 105 | 106 | # Fetch Microsoft 107 | 108 | ## Fetch vulnerability infomation 109 | 110 | ``` 111 | $ gost fetch microsoft 112 | 113 | INFO[02-24|02:13:41] Initialize Database 114 | INFO[02-24|02:13:41] Fetched all CVEs from Microsoft 115 | INFO[02-24|02:13:43] Insert Microsoft CVEs into DB db=sqlite3 116 | INFO[02-24|02:13:43] Inserting cves cves=11609 117 | 11609 / 11609 [----------------] 100.00% 3281 p/s 118 | INFO[02-24|02:13:47] Insert KB Relation relations=6016 119 | 6016 / 6016 [----------------] 100.00% 5462 p/s 120 | 121 | ``` 122 | 123 | # Server mode 124 | 125 | ``` 126 | $ gost server 127 | [Aug 15 21:38:44] INFO Opening DB (sqlite3) 128 | [Aug 15 21:38:44] INFO Migrating DB (sqlite3) 129 | [Aug 15 21:38:44] INFO Starting HTTP Server... 130 | [Aug 15 21:38:44] INFO Listening on 127.0.0.1:1325 131 | 132 | $ curl http://127.0.0.1:1325/redhat/cves/CVE-2017-1000117 | jq . [~] 133 | % Total % Received % Xferd Average Speed Time Time Time Current 134 | Dload Upload Total Spent Left Speed 135 | 100 1755 100 1755 0 0 243k 0 --:--:-- --:--:-- --:--:-- 285k 136 | { 137 | "ID": 12, 138 | "ThreatSeverity": "Important", 139 | "PublicDate": "2017-08-10T00:00:00Z", 140 | "Bugzilla": { 141 | "RedhatCVEID": 12, 142 | "description": "CVE-2017-1000117 git: Command injection via malicious ssh URLs", 143 | "id": "1480386", 144 | "url": "https://bugzilla.redhat.com/show_bug.cgi?id=1480386" 145 | }, 146 | "Cvss": { 147 | "RedhatCVEID": 0, 148 | "cvss_base_score": "", 149 | "cvss_scoring_vector": "", 150 | "status": "" 151 | }, 152 | "Cvss3": { 153 | "RedhatCVEID": 12, 154 | "cvss3_base_score": "6.3", 155 | "cvss3_scoring_vector": "CVSS:3.0/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:L/A:L", 156 | "status": "draft" 157 | }, 158 | "Iava": "", 159 | "Cwe": "", 160 | "Statement": "", 161 | "Acknowledgement": "", 162 | "Mitigation": "", 163 | "AffectedRelease": [], 164 | "PackageState": [ 165 | { 166 | "RedhatCVEID": 12, 167 | "product_name": "Red Hat Software Collections for Red Hat Enterprise Linux", 168 | "fix_state": "Affected", 169 | "package_name": "rh-git29-git", 170 | "cpe": "cpe:/a:redhat:rhel_software_collections:2" 171 | }, 172 | { 173 | "RedhatCVEID": 12, 174 | "product_name": "Red Hat Enterprise Linux 6", 175 | "fix_state": "Affected", 176 | "package_name": "git", 177 | "cpe": "cpe:/o:redhat:enterprise_linux:6" 178 | }, 179 | { 180 | "RedhatCVEID": 12, 181 | "product_name": "Red Hat Enterprise Linux 7", 182 | "fix_state": "Affected", 183 | "package_name": "git", 184 | "cpe": "cpe:/o:redhat:enterprise_linux:7" 185 | } 186 | ], 187 | "Name": "CVE-2017-1000117", 188 | "DocumentDistribution": "Copyright © 2016 Red Hat, Inc. All rights reserved.", 189 | "Details": [ 190 | { 191 | "RedhatCVEID": 12, 192 | "Detail": "Details pending" 193 | }, 194 | { 195 | "RedhatCVEID": 12, 196 | "Detail": "A shell command injection flaw related to the handling of \"ssh\" URLs has been discovered in Git. An attacker could use this flaw to execute shell commands with the privileges of the user running the Git client, for example, when performing a \"clone\" action on a malicious repository or a legitimate repository containing a malicious commit." 197 | } 198 | ], 199 | "References": [ 200 | { 201 | "RedhatCVEID": 12, 202 | "Reference": "https://lkml.org/lkml/2017/8/10/757\nhttp://blog.recurity-labs.com/2017-08-10/scm-vulns" 203 | } 204 | ] 205 | } 206 | ``` 207 | 208 | # Installation 209 | 210 | You need to install selector command (fzf or peco). 211 | 212 | ``` 213 | $ go get github.com/vulsio/gost 214 | ``` 215 | 216 | # Docker Setup, Fetch, Run as Serer and Curl 217 | 218 | ## Fetch Debian, Ubuntu, and RedHat then start as a server mode 219 | 220 | ``` 221 | $ docker run --rm -i \ 222 | -v $PWD:/gost \ 223 | -v $PWD:/var/log/gost \ 224 | vuls/gost fetch debian 225 | $ docker run --rm -i \ 226 | -v $PWD:/gost \ 227 | -v $PWD:/var/log/gost \ 228 | vuls/gost fetch ubuntu 229 | $ docker run --rm -i \ 230 | -v $PWD:/gost \ 231 | -v $PWD:/var/log/gost \ 232 | vuls/gost fetch redhat 233 | $ ls 234 | access.log gost.log gost.sqlite3 235 | 236 | $ docker run --rm -i \ 237 | -v $PWD:/gost \ 238 | -v $PWD:/var/log/gost \ 239 | -p 1325:1325 \ 240 | vuls/gost server --bind=0.0.0.0 241 | ``` 242 | 243 | ## HTTP Get to the server on Docker 244 | 245 | ``` 246 | $ curl http://127.0.0.1:1325/debian/9/pkgs/expat/unfixed-cves | jq "." Fri Jul 27 16:03:15 2018 247 | % Total % Received % Xferd Average Speed Time Time Time Current 248 | Dload Upload Total Spent Left Speed 249 | 100 970 100 970 0 0 60308 0 --:--:-- --:--:-- --:--:-- 60625 250 | { 251 | "CVE-2013-0340": { 252 | "ID": 8452, 253 | "CveID": "CVE-2013-0340", 254 | "Scope": "remote", 255 | "Description": "expat 2.1.0 and earlier does not properly handle entities expansion unless an application developer uses the XML_SetEntityDeclHandler function, which allows remote attackers to cause a denial of service (resource consumption), send HTTP requests to intranet servers, or read arbitrary files via a crafted XML document, aka an XML External Entity (XXE) issue. NOTE: it could be argued that because expat already provides the ability to disable external entity expansion, the responsibility for resolving this issue lies with application developers; according to this argument, this entry should be REJECTed, and each affected application would need its own CVE.", 256 | "Package": [ 257 | { 258 | "ID": 9829, 259 | "DebianCVEID": 8452, 260 | "PackageName": "expat", 261 | "Release": [ 262 | { 263 | "ID": 32048, 264 | "DebianPackageID": 9829, 265 | "ProductName": "stretch", 266 | "Status": "open", 267 | "FixedVersion": "", 268 | "Urgency": "unimportant", 269 | "Version": "2.2.0-2+deb9u1" 270 | } 271 | ] 272 | } 273 | ] 274 | } 275 | } 276 | ``` 277 | 278 | # Contribute 279 | 280 | 1. fork a repository: github.com/vulsio/gost to github.com/you/repo 281 | 2. get original code: `go get github.com/vulsio/gost` 282 | 3. work on original code 283 | 4. add remote to your repo: git remote add myfork https://github.com/you/repo.git 284 | 5. push your changes: git push myfork 285 | 6. create a new Pull Request 286 | 287 | - see [GitHub and Go: forking, pull requests, and go-getting](http://blog.campoy.cat/2014/03/github-and-go-forking-pull-requests-and.html) 288 | 289 | ---- 290 | 291 | # License 292 | MIT 293 | 294 | # Author 295 | Teppei Fukuda 296 | -------------------------------------------------------------------------------- /.github/workflows/fetch.yml: -------------------------------------------------------------------------------- 1 | name: Fetch Test 2 | 3 | on: 4 | pull_request: 5 | schedule: 6 | - cron: '0 0 * * *' 7 | 8 | jobs: 9 | fetch-debian: 10 | name: fetch-debian 11 | runs-on: ubuntu-latest 12 | services: 13 | mysql: 14 | image: mysql 15 | ports: 16 | - 3306:3306 17 | env: 18 | MYSQL_ROOT_PASSWORD: password 19 | MYSQL_DATABASE: test 20 | options: >- 21 | --health-cmd "mysqladmin ping" 22 | --health-interval 10s 23 | --health-timeout 5s 24 | --health-retries 5 25 | postgres: 26 | image: postgres 27 | ports: 28 | - 5432:5432 29 | env: 30 | POSTGRES_PASSWORD: password 31 | POSTGRES_DB: test 32 | options: >- 33 | --health-cmd pg_isready 34 | --health-interval 10s 35 | --health-timeout 5s 36 | --health-retries 5 37 | redis: 38 | image: redis 39 | ports: 40 | - 6379:6379 41 | options: >- 42 | --health-cmd "redis-cli ping" 43 | --health-interval 10s 44 | --health-timeout 5s 45 | --health-retries 5 46 | steps: 47 | - name: Check out code into the Go module directory 48 | uses: actions/checkout@v6 49 | - name: Set up Go 50 | uses: actions/setup-go@v6 51 | with: 52 | go-version-file: go.mod 53 | - name: build 54 | id: build 55 | run: make build 56 | - name: fetch sqlite3 57 | if: ${{ steps.build.conclusion == 'success' && ( success() || failure() )}} 58 | run: ./gost fetch --dbtype sqlite3 debian 59 | - name: fetch mysql 60 | if: ${{ steps.build.conclusion == 'success' && ( success() || failure() )}} 61 | run: ./gost fetch --dbtype mysql --dbpath "root:password@tcp(127.0.0.1:3306)/test?parseTime=true" debian 62 | - name: fetch postgres 63 | if: ${{ steps.build.conclusion == 'success' && ( success() || failure() )}} 64 | run: ./gost fetch --dbtype postgres --dbpath "host=127.0.0.1 user=postgres dbname=test sslmode=disable password=password" debian 65 | - name: fetch redis 66 | if: ${{ steps.build.conclusion == 'success' && ( success() || failure() )}} 67 | run: ./gost fetch --dbtype redis --dbpath "redis://127.0.0.1:6379/0" debian 68 | 69 | fetch-ubuntu: 70 | name: fetch-ubuntu 71 | runs-on: ubuntu-latest 72 | services: 73 | mysql: 74 | image: mysql 75 | ports: 76 | - 3306:3306 77 | env: 78 | MYSQL_ROOT_PASSWORD: password 79 | MYSQL_DATABASE: test 80 | options: >- 81 | --health-cmd "mysqladmin ping" 82 | --health-interval 10s 83 | --health-timeout 5s 84 | --health-retries 5 85 | postgres: 86 | image: postgres 87 | ports: 88 | - 5432:5432 89 | env: 90 | POSTGRES_PASSWORD: password 91 | POSTGRES_DB: test 92 | options: >- 93 | --health-cmd pg_isready 94 | --health-interval 10s 95 | --health-timeout 5s 96 | --health-retries 5 97 | redis: 98 | image: redis 99 | ports: 100 | - 6379:6379 101 | options: >- 102 | --health-cmd "redis-cli ping" 103 | --health-interval 10s 104 | --health-timeout 5s 105 | --health-retries 5 106 | steps: 107 | - name: Check out code into the Go module directory 108 | uses: actions/checkout@v6 109 | - name: Set up Go 110 | uses: actions/setup-go@v6 111 | with: 112 | go-version-file: go.mod 113 | - name: build 114 | id: build 115 | run: make build 116 | - name: fetch sqlite3 117 | if: ${{ steps.build.conclusion == 'success' && ( success() || failure() )}} 118 | run: ./gost fetch --dbtype sqlite3 ubuntu 119 | - name: fetch mysql 120 | if: ${{ steps.build.conclusion == 'success' && ( success() || failure() )}} 121 | run: ./gost fetch --dbtype mysql --dbpath "root:password@tcp(127.0.0.1:3306)/test?parseTime=true" ubuntu 122 | - name: fetch postgres 123 | if: ${{ steps.build.conclusion == 'success' && ( success() || failure() )}} 124 | run: ./gost fetch --dbtype postgres --dbpath "host=127.0.0.1 user=postgres dbname=test sslmode=disable password=password" ubuntu 125 | - name: fetch redis 126 | if: ${{ steps.build.conclusion == 'success' && ( success() || failure() )}} 127 | run: ./gost fetch --dbtype redis --dbpath "redis://127.0.0.1:6379/0" ubuntu 128 | 129 | fetch-redhat: 130 | name: fetch-redhat 131 | runs-on: ubuntu-latest 132 | services: 133 | mysql: 134 | image: mysql 135 | ports: 136 | - 3306:3306 137 | env: 138 | MYSQL_ROOT_PASSWORD: password 139 | MYSQL_DATABASE: test 140 | options: >- 141 | --health-cmd "mysqladmin ping" 142 | --health-interval 10s 143 | --health-timeout 5s 144 | --health-retries 5 145 | postgres: 146 | image: postgres 147 | ports: 148 | - 5432:5432 149 | env: 150 | POSTGRES_PASSWORD: password 151 | POSTGRES_DB: test 152 | options: >- 153 | --health-cmd pg_isready 154 | --health-interval 10s 155 | --health-timeout 5s 156 | --health-retries 5 157 | redis: 158 | image: redis 159 | ports: 160 | - 6379:6379 161 | options: >- 162 | --health-cmd "redis-cli ping" 163 | --health-interval 10s 164 | --health-timeout 5s 165 | --health-retries 5 166 | steps: 167 | - name: Check out code into the Go module directory 168 | uses: actions/checkout@v6 169 | - name: Set up Go 170 | uses: actions/setup-go@v6 171 | with: 172 | go-version-file: go.mod 173 | - name: build 174 | id: build 175 | run: make build 176 | - name: fetch sqlite3 177 | if: ${{ steps.build.conclusion == 'success' && ( success() || failure() )}} 178 | run: ./gost fetch --dbtype sqlite3 redhat 179 | - name: fetch mysql 180 | if: ${{ steps.build.conclusion == 'success' && ( success() || failure() )}} 181 | run: ./gost fetch --dbtype mysql --dbpath "root:password@tcp(127.0.0.1:3306)/test?parseTime=true" redhat 182 | - name: fetch postgres 183 | if: ${{ steps.build.conclusion == 'success' && ( success() || failure() )}} 184 | run: ./gost fetch --dbtype postgres --dbpath "host=127.0.0.1 user=postgres dbname=test sslmode=disable password=password" redhat 185 | - name: fetch redis 186 | if: ${{ steps.build.conclusion == 'success' && ( success() || failure() )}} 187 | run: ./gost fetch --dbtype redis --dbpath "redis://127.0.0.1:6379/0" redhat 188 | 189 | fetch-microsoft: 190 | name: fetch-microsoft 191 | runs-on: ubuntu-latest 192 | services: 193 | mysql: 194 | image: mysql 195 | ports: 196 | - 3306:3306 197 | env: 198 | MYSQL_ROOT_PASSWORD: password 199 | MYSQL_DATABASE: test 200 | options: >- 201 | --health-cmd "mysqladmin ping" 202 | --health-interval 10s 203 | --health-timeout 5s 204 | --health-retries 5 205 | postgres: 206 | image: postgres 207 | ports: 208 | - 5432:5432 209 | env: 210 | POSTGRES_PASSWORD: password 211 | POSTGRES_DB: test 212 | options: >- 213 | --health-cmd pg_isready 214 | --health-interval 10s 215 | --health-timeout 5s 216 | --health-retries 5 217 | redis: 218 | image: redis 219 | ports: 220 | - 6379:6379 221 | options: >- 222 | --health-cmd "redis-cli ping" 223 | --health-interval 10s 224 | --health-timeout 5s 225 | --health-retries 5 226 | steps: 227 | - name: Check out code into the Go module directory 228 | uses: actions/checkout@v6 229 | - name: Set up Go 230 | uses: actions/setup-go@v6 231 | with: 232 | go-version-file: go.mod 233 | - name: build 234 | id: build 235 | run: make build 236 | - name: fetch sqlite3 237 | if: ${{ steps.build.conclusion == 'success' && ( success() || failure() )}} 238 | run: ./gost fetch --dbtype sqlite3 microsoft 239 | - name: fetch mysql 240 | if: ${{ steps.build.conclusion == 'success' && ( success() || failure() )}} 241 | run: ./gost fetch --dbtype mysql --dbpath "root:password@tcp(127.0.0.1:3306)/test?parseTime=true" microsoft 242 | - name: fetch postgres 243 | if: ${{ steps.build.conclusion == 'success' && ( success() || failure() )}} 244 | run: ./gost fetch --dbtype postgres --dbpath "host=127.0.0.1 user=postgres dbname=test sslmode=disable password=password" microsoft 245 | - name: fetch redis 246 | if: ${{ steps.build.conclusion == 'success' && ( success() || failure() )}} 247 | run: ./gost fetch --dbtype redis --dbpath "redis://127.0.0.1:6379/0" microsoft 248 | 249 | fetch-arch: 250 | name: fetch-arch 251 | runs-on: ubuntu-latest 252 | services: 253 | mysql: 254 | image: mysql 255 | ports: 256 | - 3306:3306 257 | env: 258 | MYSQL_ROOT_PASSWORD: password 259 | MYSQL_DATABASE: test 260 | options: >- 261 | --health-cmd "mysqladmin ping" 262 | --health-interval 10s 263 | --health-timeout 5s 264 | --health-retries 5 265 | postgres: 266 | image: postgres 267 | ports: 268 | - 5432:5432 269 | env: 270 | POSTGRES_PASSWORD: password 271 | POSTGRES_DB: test 272 | options: >- 273 | --health-cmd pg_isready 274 | --health-interval 10s 275 | --health-timeout 5s 276 | --health-retries 5 277 | redis: 278 | image: redis 279 | ports: 280 | - 6379:6379 281 | options: >- 282 | --health-cmd "redis-cli ping" 283 | --health-interval 10s 284 | --health-timeout 5s 285 | --health-retries 5 286 | steps: 287 | - name: Check out code into the Go module directory 288 | uses: actions/checkout@v6 289 | - name: Set up Go 290 | uses: actions/setup-go@v6 291 | with: 292 | go-version-file: go.mod 293 | - name: build 294 | id: build 295 | run: make build 296 | - name: fetch sqlite3 297 | if: ${{ steps.build.conclusion == 'success' && ( success() || failure() )}} 298 | run: ./gost fetch --dbtype sqlite3 arch 299 | - name: fetch mysql 300 | if: ${{ steps.build.conclusion == 'success' && ( success() || failure() )}} 301 | run: ./gost fetch --dbtype mysql --dbpath "root:password@tcp(127.0.0.1:3306)/test?parseTime=true" arch 302 | - name: fetch postgres 303 | if: ${{ steps.build.conclusion == 'success' && ( success() || failure() )}} 304 | run: ./gost fetch --dbtype postgres --dbpath "host=127.0.0.1 user=postgres dbname=test sslmode=disable password=password" arch 305 | - name: fetch redis 306 | if: ${{ steps.build.conclusion == 'success' && ( success() || failure() )}} 307 | run: ./gost fetch --dbtype redis --dbpath "redis://127.0.0.1:6379/0" arch 308 | -------------------------------------------------------------------------------- /db/microsoft.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "maps" 8 | "os" 9 | "slices" 10 | "strconv" 11 | "strings" 12 | 13 | "github.com/cheggaaa/pb/v3" 14 | "github.com/inconshreveable/log15" 15 | "github.com/spf13/viper" 16 | "golang.org/x/xerrors" 17 | "gorm.io/gorm" 18 | 19 | "github.com/vulsio/gost/models" 20 | "github.com/vulsio/gost/util" 21 | ) 22 | 23 | // GetMicrosoft : 24 | func (r *RDBDriver) GetMicrosoft(cveID string) (*models.MicrosoftCVE, error) { 25 | c := models.MicrosoftCVE{} 26 | if err := r.conn. 27 | Preload("Products"). 28 | Preload("Products.ScoreSet"). 29 | Preload("Products.KBs"). 30 | Where(&models.MicrosoftCVE{CveID: cveID}). 31 | Take(&c).Error; err != nil { 32 | if errors.Is(err, gorm.ErrRecordNotFound) { 33 | return nil, nil 34 | } 35 | log15.Error("Failed to get Microsoft", "err", err) 36 | return nil, err 37 | } 38 | return &c, nil 39 | } 40 | 41 | // GetMicrosoftMulti : 42 | func (r *RDBDriver) GetMicrosoftMulti(cveIDs []string) (map[string]models.MicrosoftCVE, error) { 43 | cs := []models.MicrosoftCVE{} 44 | if err := r.conn. 45 | Preload("Products"). 46 | Preload("Products.ScoreSet"). 47 | Preload("Products.KBs"). 48 | Where("cve_id IN ?", cveIDs). 49 | Find(&cs).Error; err != nil { 50 | log15.Error("Failed to get Microsoft", "err", err) 51 | return nil, err 52 | } 53 | 54 | m := map[string]models.MicrosoftCVE{} 55 | for _, c := range cs { 56 | m[c.CveID] = c 57 | } 58 | return m, nil 59 | } 60 | 61 | // GetExpandKB : 62 | func (r *RDBDriver) GetExpandKB(applied []string, unapplied []string) ([]string, []string, error) { 63 | uniqAppliedKBIDs := map[string]struct{}{} 64 | uniqUnappliedKBIDs := map[string]struct{}{} 65 | for _, kbID := range applied { 66 | uniqAppliedKBIDs[kbID] = struct{}{} 67 | } 68 | for _, kbID := range unapplied { 69 | uniqUnappliedKBIDs[kbID] = struct{}{} 70 | delete(uniqAppliedKBIDs, kbID) 71 | } 72 | applied = slices.Collect(maps.Keys(uniqAppliedKBIDs)) 73 | 74 | if len(applied) > 0 { 75 | relations := []models.MicrosoftKBRelation{} 76 | 77 | if err := r.conn. 78 | Preload("SupersededBy"). 79 | Where("kb_id IN ?", applied). 80 | Find(&relations).Error; err != nil { 81 | return nil, nil, xerrors.Errorf("Failed to get KB Relation by applied KBID: %q. err: %w", applied, err) 82 | } 83 | 84 | for _, relation := range relations { 85 | isInApplied := false 86 | for _, supersededby := range relation.SupersededBy { 87 | if slices.Contains(applied, supersededby.KBID) { 88 | isInApplied = true 89 | break 90 | } 91 | } 92 | if !isInApplied { 93 | for _, supersededby := range relation.SupersededBy { 94 | uniqUnappliedKBIDs[supersededby.KBID] = struct{}{} 95 | } 96 | } 97 | } 98 | } 99 | 100 | if len(uniqUnappliedKBIDs) > 0 { 101 | relations := []models.MicrosoftKBRelation{} 102 | 103 | if err := r.conn. 104 | Preload("SupersededBy"). 105 | Where("kb_id IN ?", slices.Collect(maps.Keys(uniqUnappliedKBIDs))). 106 | Find(&relations).Error; err != nil { 107 | return nil, nil, xerrors.Errorf("Failed to get KB Relation by unapplied KBID: %q. err: %w", unapplied, err) 108 | } 109 | 110 | for _, relation := range relations { 111 | for _, supersededby := range relation.SupersededBy { 112 | uniqUnappliedKBIDs[supersededby.KBID] = struct{}{} 113 | } 114 | } 115 | } 116 | 117 | return applied, slices.Collect(maps.Keys(uniqUnappliedKBIDs)), nil 118 | } 119 | 120 | // GetRelatedProducts : 121 | func (r *RDBDriver) GetRelatedProducts(release string, kbs []string) ([]string, error) { 122 | if len(kbs) == 0 { 123 | return []string{}, nil 124 | } 125 | 126 | var products []string 127 | if err := r.conn. 128 | Model(&models.MicrosoftProduct{}). 129 | Distinct("microsoft_products.name"). 130 | Joins("JOIN microsoft_kbs ON microsoft_kbs.microsoft_product_id = microsoft_products.id AND microsoft_kbs.article IN ?", kbs). 131 | Find(&products).Error; err != nil { 132 | return nil, xerrors.Errorf("Failed to detect Products. err: %w", err) 133 | } 134 | 135 | if release == "" { 136 | return products, nil 137 | } 138 | var filtered []string 139 | for _, p := range products { 140 | switch { 141 | case strings.Contains(p, "Microsoft Windows 2000"), // Microsoft Windows 2000; Microsoft Windows 2000 Server 142 | strings.Contains(p, "Microsoft Windows XP"), // Microsoft Windows XP 143 | strings.Contains(p, "Microsoft Windows Server 2003"), // Microsoft Windows Server 2003; Microsoft Windows Server 2003 R2 144 | strings.Contains(p, "Windows Vista"), // Windows Vista 145 | strings.Contains(p, "Windows Server 2008"), // Windows Server 2008; Windows Server 2008 R2 146 | strings.Contains(p, "Windows 7"), // Windows 7 147 | strings.Contains(p, "Windows 8"), // Windows 8 148 | strings.Contains(p, "Windows Server 2012"), // Windows Server 2012; Windows Server 2012 R2 149 | strings.Contains(p, "Windows 8.1"), // Windows 8.1 150 | strings.Contains(p, "Windows RT 8.1"), // Windows RT 8.1 151 | strings.Contains(p, "Windows 10"), // Windows 10 152 | strings.Contains(p, "Windows 11"), // Windows 11 153 | strings.Contains(p, "Windows Server 2016"), // Windows Server 2016 154 | strings.Contains(p, "Windows Server 2019"), // Windows Server 2019 155 | strings.Contains(p, "Windows Server, Version"), // Windows Server, Version 156 | strings.Contains(p, "Windows Server 2022"): // Windows Server 2022 157 | if strings.HasSuffix(p, release) { 158 | filtered = append(filtered, p) 159 | } 160 | default: 161 | filtered = append(filtered, p) 162 | } 163 | } 164 | return filtered, nil 165 | } 166 | 167 | // GetFilteredCvesMicrosoft : 168 | func (r *RDBDriver) GetFilteredCvesMicrosoft(products []string, kbs []string) (map[string]models.MicrosoftCVE, error) { 169 | var q *gorm.DB 170 | if len(products) > 0 { 171 | q = r.conn.Preload("Products", "name IN ?", products) 172 | } else { 173 | q = r.conn.Preload("Products") 174 | } 175 | 176 | cves, cs := []models.MicrosoftCVE{}, []models.MicrosoftCVE{} 177 | if err := q. 178 | Preload("Products.ScoreSet"). 179 | Preload("Products.KBs"). 180 | FindInBatches(&cs, 998, func(_ *gorm.DB, _ int) error { 181 | cves = append(cves, cs...) 182 | return nil 183 | }).Error; err != nil { 184 | return nil, xerrors.Errorf("Failed to get Microsoft. err: %w", err) 185 | } 186 | 187 | detected := map[string]models.MicrosoftCVE{} 188 | for _, c := range cves { 189 | ps := []models.MicrosoftProduct{} 190 | for _, p := range c.Products { 191 | if len(kbs) == 0 || len(p.KBs) == 0 { 192 | ps = append(ps, p) 193 | continue 194 | } 195 | 196 | filtered := []models.MicrosoftKB{} 197 | for _, kb := range p.KBs { 198 | if _, err := strconv.Atoi(kb.Article); err != nil { 199 | filtered = append(filtered, kb) 200 | } else if slices.Contains(kbs, kb.Article) { 201 | filtered = append(filtered, kb) 202 | } 203 | } 204 | if len(filtered) > 0 { 205 | p.KBs = filtered 206 | ps = append(ps, p) 207 | } 208 | } 209 | if len(ps) > 0 { 210 | c.Products = ps 211 | detected[c.CveID] = c 212 | } 213 | } 214 | return detected, nil 215 | } 216 | 217 | // GetAdvisoriesMicrosoft gets AdvisoryID: []CVE IDs 218 | func (r *RDBDriver) GetAdvisoriesMicrosoft() (map[string][]string, error) { 219 | m := map[string][]string{} 220 | var cs []models.MicrosoftCVE 221 | // the maximum value of a host parameter number is SQLITE_MAX_VARIABLE_NUMBER, which defaults to 999 for SQLite versions prior to 3.32.0 (2020-05-22) or 32766 for SQLite versions after 3.32.0. 222 | // https://www.sqlite.org/limits.html Maximum Number Of Host Parameters In A Single SQL Statement 223 | if err := r.conn.Preload("Products").Preload("Products.KBs").FindInBatches(&cs, 999, func(_ *gorm.DB, _ int) error { 224 | for _, c := range cs { 225 | for _, p := range c.Products { 226 | for _, kb := range p.KBs { 227 | if _, err := strconv.Atoi(kb.Article); err == nil { 228 | m[kb.Article] = append(m[kb.Article], c.CveID) 229 | } 230 | } 231 | } 232 | } 233 | return nil 234 | }).Error; err != nil { 235 | return nil, xerrors.Errorf("Failed to get Microsoft. err: %w", err) 236 | } 237 | 238 | for k := range m { 239 | m[k] = util.Unique(m[k]) 240 | } 241 | 242 | return m, nil 243 | } 244 | 245 | // InsertMicrosoft : 246 | func (r *RDBDriver) InsertMicrosoft(cves []models.MicrosoftCVE, relations []models.MicrosoftKBRelation) error { 247 | log15.Info("Inserting cves", "cves", len(cves)) 248 | if err := r.deleteAndInsertMicrosoft(cves); err != nil { 249 | return xerrors.Errorf("Failed to insert Microsoft CVE data. err: %w", err) 250 | } 251 | log15.Info("Insert KB Relation", "relations", len(relations)) 252 | if err := r.deleteAndInsertMicrosoftKBRelation(relations); err != nil { 253 | return xerrors.Errorf("Failed to insert Microsoft KB Relation data. err: %w", err) 254 | } 255 | return nil 256 | } 257 | 258 | func (r *RDBDriver) deleteAndInsertMicrosoft(cves []models.MicrosoftCVE) (err error) { 259 | bar := pb.StartNew(len(cves)).SetWriter(func() io.Writer { 260 | if viper.GetBool("log-json") { 261 | return io.Discard 262 | } 263 | return os.Stderr 264 | }()) 265 | tx := r.conn.Begin() 266 | 267 | defer func() { 268 | if err != nil { 269 | tx.Rollback() 270 | return 271 | } 272 | tx.Commit() 273 | }() 274 | 275 | // Delete all old records 276 | for _, table := range []interface{}{models.MicrosoftCVE{}, models.MicrosoftProduct{}, models.MicrosoftScoreSet{}, models.MicrosoftKB{}} { 277 | if err := tx.Session(&gorm.Session{AllowGlobalUpdate: true}).Delete(table).Error; err != nil { 278 | return xerrors.Errorf("Failed to delete old records. err: %w", err) 279 | } 280 | } 281 | 282 | batchSize := viper.GetInt("batch-size") 283 | if batchSize < 1 { 284 | return fmt.Errorf("Failed to set batch-size. err: batch-size option is not set properly") 285 | } 286 | 287 | for chunk := range slices.Chunk(cves, batchSize) { 288 | if err = tx.Create(chunk).Error; err != nil { 289 | return xerrors.Errorf("Failed to insert. err: %w", err) 290 | } 291 | bar.Add(len(chunk)) 292 | } 293 | bar.Finish() 294 | 295 | return nil 296 | } 297 | 298 | func (r *RDBDriver) deleteAndInsertMicrosoftKBRelation(kbs []models.MicrosoftKBRelation) (err error) { 299 | bar := pb.StartNew(len(kbs)).SetWriter(func() io.Writer { 300 | if viper.GetBool("log-json") { 301 | return io.Discard 302 | } 303 | return os.Stderr 304 | }()) 305 | tx := r.conn.Begin() 306 | 307 | defer func() { 308 | if err != nil { 309 | tx.Rollback() 310 | return 311 | } 312 | tx.Commit() 313 | }() 314 | 315 | // Delete all old records 316 | for _, table := range []interface{}{models.MicrosoftKBRelation{}, models.MicrosoftSupersededBy{}} { 317 | if err := tx.Session(&gorm.Session{AllowGlobalUpdate: true}).Delete(table).Error; err != nil { 318 | return xerrors.Errorf("Failed to delete old records. err: %w", err) 319 | } 320 | } 321 | 322 | batchSize := viper.GetInt("batch-size") 323 | if batchSize < 1 { 324 | return fmt.Errorf("Failed to set batch-size. err: batch-size option is not set properly") 325 | } 326 | 327 | for chunk := range slices.Chunk(kbs, batchSize) { 328 | if err = tx.Create(chunk).Error; err != nil { 329 | return xerrors.Errorf("Failed to insert. err: %w", err) 330 | } 331 | bar.Add(len(chunk)) 332 | } 333 | bar.Finish() 334 | return nil 335 | } 336 | -------------------------------------------------------------------------------- /server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "os" 7 | "path/filepath" 8 | 9 | "github.com/inconshreveable/log15" 10 | "github.com/labstack/echo/v4" 11 | "github.com/labstack/echo/v4/middleware" 12 | "github.com/spf13/viper" 13 | "golang.org/x/xerrors" 14 | 15 | "github.com/vulsio/gost/db" 16 | "github.com/vulsio/gost/util" 17 | ) 18 | 19 | // Start starts CVE dictionary HTTP Server. 20 | func Start(logToFile bool, logDir string, driver db.DB) error { 21 | e := echo.New() 22 | e.Debug = viper.GetBool("debug") 23 | 24 | // Middleware 25 | e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{Output: os.Stderr})) 26 | e.Use(middleware.Recover()) 27 | 28 | // setup access logger 29 | if logToFile { 30 | logPath := filepath.Join(logDir, "access.log") 31 | f, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) 32 | if err != nil { 33 | return xerrors.Errorf("Failed to open a log file: %s", err) 34 | } 35 | defer f.Close() 36 | e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{Output: f})) 37 | } 38 | 39 | // Routes 40 | e.GET("/health", health()) 41 | e.GET("/redhat/cves/:id", getRedhatCve(driver)) 42 | e.GET("/debian/cves/:id", getDebianCve(driver)) 43 | e.GET("/ubuntu/cves/:id", getUbuntuCve(driver)) 44 | e.GET("/microsoft/cves/:id", getMicrosoftCve(driver)) 45 | e.GET("/arch/advs/:id", getArchAdv(driver)) 46 | e.POST("/redhat/multi-cves", getRedhatMultiCve(driver)) 47 | e.POST("/debian/multi-cves", getDebianMultiCve(driver)) 48 | e.POST("/ubuntu/multi-cves", getUbuntuMultiCve(driver)) 49 | e.POST("/microsoft/multi-cves", getMicrosoftMultiCve(driver)) 50 | e.POST("/arch/multi-advs", getArchMultiAdv(driver)) 51 | e.GET("/redhat/:release/pkgs/:name/unfixed-cves", getUnfixedCvesRedhat(driver)) 52 | e.GET("/debian/:release/pkgs/:name/unfixed-cves", getUnfixedCvesDebian(driver)) 53 | e.GET("/debian/:release/pkgs/:name/fixed-cves", getFixedCvesDebian(driver)) 54 | e.GET("/ubuntu/:release/pkgs/:name/unfixed-cves", getUnfixedCvesUbuntu(driver)) 55 | e.GET("/ubuntu/:release/pkgs/:name/fixed-cves", getFixedCvesUbuntu(driver)) 56 | e.GET("/redhat/advisories", getRedhatAdvisories(driver)) 57 | e.GET("/ubuntu/advisories", getUbuntuAdvisories(driver)) 58 | e.GET("/microsoft/advisories", getMicrosoftAdvisories(driver)) 59 | e.GET("/arch/advisories", getArchAdvisories(driver)) 60 | e.POST("/microsoft/kbs", getExpandKB(driver)) 61 | e.POST("/microsoft/products", getRelatedProducts(driver)) 62 | e.POST("/microsoft/filtered-cves", getFilteredCvesMicrosoft(driver)) 63 | e.GET("/arch/pkgs/:name/unfixed-advs", getUnfixedAdvsArch(driver)) 64 | e.GET("/arch/pkgs/:name/fixed-advs", getFixedAdvsArch(driver)) 65 | 66 | bindURL := fmt.Sprintf("%s:%s", viper.GetString("bind"), viper.GetString("port")) 67 | log15.Info("Listening", "URL", bindURL) 68 | 69 | return e.Start(bindURL) 70 | } 71 | 72 | // Handler 73 | func health() echo.HandlerFunc { 74 | return func(c echo.Context) error { 75 | return c.String(http.StatusOK, "") 76 | } 77 | } 78 | 79 | // Handler 80 | func getRedhatCve(driver db.DB) echo.HandlerFunc { 81 | return func(c echo.Context) error { 82 | cveid := c.Param("id") 83 | cveDetail, err := driver.GetRedhat(cveid) 84 | if err != nil { 85 | log15.Error("Failed to get RedHat by CVEID.", "err", err) 86 | return err 87 | } 88 | return c.JSON(http.StatusOK, &cveDetail) 89 | } 90 | } 91 | 92 | // Handler 93 | func getDebianCve(driver db.DB) echo.HandlerFunc { 94 | return func(c echo.Context) error { 95 | cveid := c.Param("id") 96 | cveDetail, err := driver.GetDebian(cveid) 97 | if err != nil { 98 | log15.Error("Failed to get Debian by CVEID.", "err", err) 99 | return err 100 | } 101 | return c.JSON(http.StatusOK, &cveDetail) 102 | } 103 | } 104 | 105 | // Handler 106 | func getUbuntuCve(driver db.DB) echo.HandlerFunc { 107 | return func(c echo.Context) error { 108 | cveid := c.Param("id") 109 | cveDetail, err := driver.GetUbuntu(cveid) 110 | if err != nil { 111 | log15.Error("Failed to get Ubuntu by CVEID.", "err", err) 112 | return err 113 | } 114 | return c.JSON(http.StatusOK, &cveDetail) 115 | } 116 | } 117 | 118 | // Handler 119 | func getMicrosoftCve(driver db.DB) echo.HandlerFunc { 120 | return func(c echo.Context) error { 121 | cveid := c.Param("id") 122 | cveDetail, err := driver.GetMicrosoft(cveid) 123 | if err != nil { 124 | log15.Error("Failed to get Microsoft by CVEID.", "err", err) 125 | return err 126 | } 127 | return c.JSON(http.StatusOK, &cveDetail) 128 | } 129 | } 130 | 131 | // Handler 132 | func getArchAdv(driver db.DB) echo.HandlerFunc { 133 | return func(c echo.Context) error { 134 | cveid := c.Param("id") 135 | cveDetail, err := driver.GetArch(cveid) 136 | if err != nil { 137 | log15.Error("Failed to get Arch by Advisory ID.", "err", err) 138 | return err 139 | } 140 | return c.JSON(http.StatusOK, &cveDetail) 141 | } 142 | } 143 | 144 | type cveIDs struct { 145 | CveIDs []string `json:"cveIDs"` 146 | } 147 | 148 | // Handler 149 | func getRedhatMultiCve(driver db.DB) echo.HandlerFunc { 150 | return func(c echo.Context) error { 151 | cveIDs := cveIDs{} 152 | if err := c.Bind(&cveIDs); err != nil { 153 | return err 154 | } 155 | cveDetails, err := driver.GetRedhatMulti(cveIDs.CveIDs) 156 | if err != nil { 157 | log15.Error("Failed to get RedHat by CVEIDs.", "err", err) 158 | return err 159 | } 160 | return c.JSON(http.StatusOK, &cveDetails) 161 | } 162 | } 163 | 164 | // Handler 165 | func getDebianMultiCve(driver db.DB) echo.HandlerFunc { 166 | return func(c echo.Context) error { 167 | cveIDs := cveIDs{} 168 | if err := c.Bind(&cveIDs); err != nil { 169 | return err 170 | } 171 | cveDetails, err := driver.GetDebianMulti(cveIDs.CveIDs) 172 | if err != nil { 173 | log15.Error("Failed to get Debian by CVEIDs.", "err", err) 174 | return err 175 | } 176 | return c.JSON(http.StatusOK, &cveDetails) 177 | } 178 | } 179 | 180 | // Handler 181 | func getUbuntuMultiCve(driver db.DB) echo.HandlerFunc { 182 | return func(c echo.Context) error { 183 | cveIDs := cveIDs{} 184 | if err := c.Bind(&cveIDs); err != nil { 185 | return err 186 | } 187 | cveDetails, err := driver.GetUbuntuMulti(cveIDs.CveIDs) 188 | if err != nil { 189 | log15.Error("Failed to get Ubuntu by CVEIDs.", "err", err) 190 | return err 191 | } 192 | return c.JSON(http.StatusOK, &cveDetails) 193 | } 194 | } 195 | 196 | // Handler 197 | func getMicrosoftMultiCve(driver db.DB) echo.HandlerFunc { 198 | return func(c echo.Context) error { 199 | cveIDs := cveIDs{} 200 | if err := c.Bind(&cveIDs); err != nil { 201 | return err 202 | } 203 | cveDetails, err := driver.GetMicrosoftMulti(cveIDs.CveIDs) 204 | if err != nil { 205 | log15.Error("Failed to get Microsoft by CVEIDs.", "err", err) 206 | return err 207 | } 208 | return c.JSON(http.StatusOK, &cveDetails) 209 | } 210 | } 211 | 212 | // Handler 213 | func getArchMultiAdv(driver db.DB) echo.HandlerFunc { 214 | type advIDs struct { 215 | AdvIDs []string `json:"advIDs"` 216 | } 217 | 218 | return func(c echo.Context) error { 219 | var advIDs advIDs 220 | if err := c.Bind(&advIDs); err != nil { 221 | return err 222 | } 223 | cveDetails, err := driver.GetArchMulti(advIDs.AdvIDs) 224 | if err != nil { 225 | log15.Error("Failed to get Arch by Advisory IDs.", "err", err) 226 | return err 227 | } 228 | return c.JSON(http.StatusOK, &cveDetails) 229 | } 230 | } 231 | 232 | // Handler 233 | func getUnfixedCvesRedhat(driver db.DB) echo.HandlerFunc { 234 | return func(c echo.Context) error { 235 | release := util.Major(c.Param("release")) 236 | pkgName := c.Param("name") 237 | cveDetail, err := driver.GetUnfixedCvesRedhat(release, pkgName, false) 238 | if err != nil { 239 | log15.Error("Failed to get Unfixed CVEs in RedHat", "err", err) 240 | return err 241 | } 242 | return c.JSON(http.StatusOK, &cveDetail) 243 | } 244 | } 245 | 246 | // Handler 247 | func getUnfixedCvesDebian(driver db.DB) echo.HandlerFunc { 248 | return func(c echo.Context) error { 249 | release := util.Major(c.Param("release")) 250 | pkgName := c.Param("name") 251 | cveDetail, err := driver.GetUnfixedCvesDebian(release, pkgName) 252 | if err != nil { 253 | log15.Error("Failed to get Unfixed CVEs in Debian", "err", err) 254 | return err 255 | } 256 | return c.JSON(http.StatusOK, &cveDetail) 257 | } 258 | } 259 | 260 | // Handler 261 | func getFixedCvesDebian(driver db.DB) echo.HandlerFunc { 262 | return func(c echo.Context) error { 263 | release := util.Major(c.Param("release")) 264 | pkgName := c.Param("name") 265 | cveDetail, err := driver.GetFixedCvesDebian(release, pkgName) 266 | if err != nil { 267 | log15.Error("Failed to get Fixed CVEs in Debian", "err", err) 268 | return err 269 | } 270 | return c.JSON(http.StatusOK, &cveDetail) 271 | } 272 | } 273 | 274 | // Handler 275 | func getUnfixedCvesUbuntu(driver db.DB) echo.HandlerFunc { 276 | return func(c echo.Context) error { 277 | release := util.Major(c.Param("release")) 278 | pkgName := c.Param("name") 279 | cveDetail, err := driver.GetUnfixedCvesUbuntu(release, pkgName) 280 | if err != nil { 281 | log15.Error("Failed to get Unfixed CVEs in Ubuntu", "err", err) 282 | return err 283 | } 284 | return c.JSON(http.StatusOK, &cveDetail) 285 | } 286 | } 287 | 288 | // Handler 289 | func getFixedCvesUbuntu(driver db.DB) echo.HandlerFunc { 290 | return func(c echo.Context) error { 291 | release := util.Major(c.Param("release")) 292 | pkgName := c.Param("name") 293 | cveDetail, err := driver.GetFixedCvesUbuntu(release, pkgName) 294 | if err != nil { 295 | log15.Error("Failed to get Fixed CVEs in Ubuntu", "err", err) 296 | return err 297 | } 298 | return c.JSON(http.StatusOK, &cveDetail) 299 | } 300 | } 301 | 302 | // Handler 303 | func getExpandKB(driver db.DB) echo.HandlerFunc { 304 | return func(c echo.Context) error { 305 | var b struct { 306 | Applied []string `json:"applied"` 307 | Unapplied []string `json:"unapplied"` 308 | } 309 | if err := c.Bind(&b); err != nil { 310 | return err 311 | } 312 | applied, unapplied, err := driver.GetExpandKB(b.Applied, b.Unapplied) 313 | if err != nil { 314 | log15.Error("Failed to expand KB", "err", err) 315 | return err 316 | } 317 | return c.JSON(http.StatusOK, struct { 318 | Applied []string `json:"applied"` 319 | Unapplied []string `json:"unapplied"` 320 | }{Applied: applied, Unapplied: unapplied}) 321 | } 322 | } 323 | 324 | // Handler 325 | func getRelatedProducts(driver db.DB) echo.HandlerFunc { 326 | return func(c echo.Context) error { 327 | var b struct { 328 | Release string `json:"release"` 329 | KBs []string `json:"kbs"` 330 | } 331 | if err := c.Bind(&b); err != nil { 332 | return err 333 | } 334 | products, err := driver.GetRelatedProducts(b.Release, b.KBs) 335 | if err != nil { 336 | log15.Error("Failed to get related products", "err", err) 337 | return err 338 | } 339 | return c.JSON(http.StatusOK, products) 340 | } 341 | } 342 | 343 | // Handler 344 | func getFilteredCvesMicrosoft(driver db.DB) echo.HandlerFunc { 345 | return func(c echo.Context) error { 346 | var b struct { 347 | Products []string `json:"products"` 348 | KBs []string `json:"kbs"` 349 | } 350 | if err := c.Bind(&b); err != nil { 351 | return err 352 | } 353 | cves, err := driver.GetFilteredCvesMicrosoft(b.Products, b.KBs) 354 | if err != nil { 355 | log15.Error("Failed to get cves", "err", err) 356 | return err 357 | } 358 | return c.JSON(http.StatusOK, &cves) 359 | } 360 | } 361 | 362 | // Handler 363 | func getRedhatAdvisories(driver db.DB) echo.HandlerFunc { 364 | return func(c echo.Context) error { 365 | m, err := driver.GetAdvisoriesRedHat() 366 | if err != nil { 367 | log15.Error("Failed to get RedHat Advisories.", "err", err) 368 | return err 369 | } 370 | return c.JSON(http.StatusOK, &m) 371 | } 372 | } 373 | 374 | // Handler 375 | func getUbuntuAdvisories(driver db.DB) echo.HandlerFunc { 376 | return func(c echo.Context) error { 377 | m, err := driver.GetAdvisoriesUbuntu() 378 | if err != nil { 379 | log15.Error("Failed to get Ubuntu Advisories.", "err", err) 380 | return err 381 | } 382 | return c.JSON(http.StatusOK, &m) 383 | } 384 | } 385 | 386 | // Handler 387 | func getMicrosoftAdvisories(driver db.DB) echo.HandlerFunc { 388 | return func(c echo.Context) error { 389 | m, err := driver.GetAdvisoriesMicrosoft() 390 | if err != nil { 391 | log15.Error("Failed to get Microsoft Advisories.", "err", err) 392 | return err 393 | } 394 | return c.JSON(http.StatusOK, &m) 395 | } 396 | } 397 | 398 | // Handler 399 | func getArchAdvisories(driver db.DB) echo.HandlerFunc { 400 | return func(c echo.Context) error { 401 | m, err := driver.GetAdvisoriesArch() 402 | if err != nil { 403 | log15.Error("Failed to get Arch Advisories.", "err", err) 404 | return err 405 | } 406 | return c.JSON(http.StatusOK, &m) 407 | } 408 | } 409 | 410 | // Handler 411 | func getUnfixedAdvsArch(driver db.DB) echo.HandlerFunc { 412 | return func(c echo.Context) error { 413 | pkgName := c.Param("name") 414 | cveDetail, err := driver.GetUnfixedAdvsArch(pkgName) 415 | if err != nil { 416 | log15.Error("Failed to get Unfixed Advisories in Arch", "err", err) 417 | return err 418 | } 419 | return c.JSON(http.StatusOK, &cveDetail) 420 | } 421 | } 422 | 423 | // Handler 424 | func getFixedAdvsArch(driver db.DB) echo.HandlerFunc { 425 | return func(c echo.Context) error { 426 | pkgName := c.Param("name") 427 | cveDetail, err := driver.GetFixedAdvsArch(pkgName) 428 | if err != nil { 429 | log15.Error("Failed to get Fixed Advisories in Arch", "err", err) 430 | return err 431 | } 432 | return c.JSON(http.StatusOK, &cveDetail) 433 | } 434 | } 435 | --------------------------------------------------------------------------------