Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
mprpic committed Sep 6, 2023
0 parents commit b5196b6
Show file tree
Hide file tree
Showing 13 changed files with 631 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
bin/
dist/
46 changes: 46 additions & 0 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# This is an example .goreleaser.yml file with some sensible defaults.
# Make sure to check the documentation at https://goreleaser.com
before:
hooks:
# You may remove this if you don't use go modules.
- go mod tidy
# you may remove this if you don't need go generate
- go generate ./...
builds:
- env:
- CGO_ENABLED=0
goos:
- linux
- windows
- darwin
main: cmd/cvelint/main.go

archives:
- format: tar.gz
# this name template makes the OS and Arch compatible with the results of uname.
name_template: >-
{{ .ProjectName }}_
{{- title .Os }}_
{{- if eq .Arch "amd64" }}x86_64
{{- else if eq .Arch "386" }}i386
{{- else }}{{ .Arch }}{{ end }}
{{- if .Arm }}v{{ .Arm }}{{ end }}
# use zip for windows archives
format_overrides:
- goos: windows
format: zip
checksum:
name_template: 'checksums.txt'
snapshot:
name_template: "{{ incpatch .Version }}-next"
changelog:
sort: asc
filters:
exclude:
- '^docs:'
- '^test:'

# The lines beneath this are called `modelines`. See `:help modeline`
# Feel free to remove those if you don't want/use them.
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
# vim: set ts=2 sw=2 tw=0 fo=cnqoj
5 changes: 5 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
build:
go build -o bin/cvelint cmd/cvelint/main.go

clean:
/bin/rm -f bin/cvelint
132 changes: 132 additions & 0 deletions cmd/cvelint/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package main

import (
"flag"
"fmt"
"github.com/mprpic/cvelint/internal"
"io/fs"
"log"
"os"
"path/filepath"
"sort"
"strings"
"time"
)

func collectFiles(args []string) ([]string, error) {
var files []string
var dir string
if len(args) == 0 {
dir = "."
} else {
info, err := os.Stat(args[0])
if err != nil {
return files, err
}
if info.IsDir() {
dir = args[0]
} else {
if filepath.Ext(info.Name()) != ".json" {
return files, fmt.Errorf("ERROR: \"%s\" is not a JSON file", args[0])
}
files = append(files, args[0])
return files, nil
}
}
err := filepath.WalkDir(dir, func(f string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if !d.IsDir() && filepath.Ext(d.Name()) == ".json" {
files = append(files, f)
}
return nil
})
return files, err
}

func main() {
log.SetFlags(0)

flag.Usage = func() {
w := flag.CommandLine.Output()
fmt.Fprintf(w, "Usage of %s: [OPTION] [DIRECTORY|FILE]\n", os.Args[0])
flag.PrintDefaults()
}

var format string
flag.StringVar(&format, "format", "text", "Output format: text, json, csv")

var cna string
flag.StringVar(&cna, "cna", "", "Show results for CVE records of a specific CNA")

var selectRules string
flag.StringVar(&selectRules, "select", "", "Comma-separated list of rule codes to enable (default: all)")

var ignoreRules string
flag.StringVar(&ignoreRules, "ignore", "", "Comma-separated list of rule codes to disable (default: none)")

var printRules bool
flag.BoolVar(&printRules, "show-rules", false, "Print list of available validation rules")

flag.Parse()
args := flag.Args()

if printRules {
var codes []string
for code := range internal.RuleSet {
codes = append(codes, code)
}
sort.Strings(codes)
for _, code := range codes {
fmt.Printf("%s: %s\n", code, internal.RuleSet[code].Description)
}
os.Exit(0)
}

if len(args) != 1 {
fmt.Println("ERROR: Incorrect number of arguments")
flag.Usage()
os.Exit(1)
}

files, err := collectFiles(args)
if err != nil {
log.Fatalf("ERROR: %s", err)
}
if len(files) == 0 {
log.Fatal("ERROR: no CVE record JSON files found")
}

var ruleCodes = make(map[string]struct{})
if selectRules != "" {
// Select unique specified rule codes
for _, ruleCode := range strings.Split(selectRules, ",") {
ruleCodes[ruleCode] = struct{}{}
}
} else {
// Select all rule codes
for ruleCode, _ := range internal.RuleSet {
ruleCodes[ruleCode] = struct{}{}
}
}
// Remove ignored rule codes
for _, ruleCode := range strings.Split(ignoreRules, ",") {
delete(ruleCodes, ruleCode)
}

// Collect Rules from specified rule codes
var rules []internal.Rule
for ruleCode, _ := range ruleCodes {
rule, ok := internal.RuleSet[ruleCode]
if !ok {
log.Fatalf("ERROR: unknown rule selected: %s", ruleCode)
} else {
rules = append(rules, rule)
}
}

linter := internal.Linter{Timestamp: time.Now().UTC(), FileInput: &files}
linter.Run(&rules, cna)
linter.Print(format)
}
16 changes: 16 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
module github.com/mprpic/cvelint

go 1.20

require (
github.com/fatih/color v1.15.0
github.com/tidwall/gjson v1.16.0
)

require (
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
golang.org/x/sys v0.6.0 // indirect
)
17 changes: 17 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/tidwall/gjson v1.16.0 h1:SyXa+dsSPpUlcwEDuKuEBJEz5vzTvOea+9rjyYodQFg=
github.com/tidwall/gjson v1.16.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
151 changes: 151 additions & 0 deletions internal/linter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package internal

import (
"encoding/json"
"fmt"
"github.com/fatih/color"
"github.com/mprpic/cvelint/internal/rules"
"github.com/tidwall/gjson"
"log"
"os"
"sort"
"strconv"
"strings"
"time"
)

type Linter struct {
Timestamp time.Time
FileInput *[]string
FilesChecked int
Results []LintResult
}

type LintResult struct {
File string
CveId string
Cna string
Error rules.ValidationError
Rule
}

func (l *Linter) Run(rules *[]Rule, cna string) {
checkedFiles := 0
for _, file := range *l.FileInput {
cveId := strings.TrimSuffix(file[strings.LastIndex(file, "/")+1:], ".json")
jsonBytes, err := os.ReadFile(file)
// Convert to string because gjson.Result.Path does not accept []byte
json := string(jsonBytes)
if err != nil {
log.Fatalf("ERROR: failed to read JSON file %v", err)
}
if !gjson.Valid(json) {
log.Fatalf("ERROR: invalid JSON file %s", file)
}
recordCna := gjson.Get(json, "cveMetadata.assignerShortName").String()
if recordCna == "" {
// Not a CVE v5 JSON record, skip.
continue
}
if cna != "" && cna != recordCna {
continue
}
for _, rule := range *rules {
errors := rule.CheckFunc(&json)
for _, e := range errors {
l.Results = append(l.Results, LintResult{
File: file,
CveId: cveId,
Cna: recordCna,
Error: e,
Rule: rule,
})
}
}
checkedFiles++

sort.Slice(l.Results, func(i, j int) bool {
// Sort results alphanumerically by CVE ID (starting from newest)
a := strings.Split(l.Results[i].CveId, "-") // CVE-2020-0001 -> [CVE 2020 0001]
b := strings.Split(l.Results[j].CveId, "-")
i, _ = strconv.Atoi(strings.Join(a[1:], "")) // 20200001
j, _ = strconv.Atoi(strings.Join(b[1:], ""))
return i > j
})
}
l.FilesChecked = checkedFiles
}

func (l *Linter) Print(format string) {
switch format {
case "text":
fmt.Printf("")
fmt.Printf("Collected %d file", len(*l.FileInput))
if len(*l.FileInput) != 1 {
fmt.Print("s")
}
fmt.Printf("; checked %d file", l.FilesChecked)
if l.FilesChecked != 1 {
fmt.Println("s.")
} else {
fmt.Println(".")
}

bold := color.New(color.Bold).Add(color.Underline)
red := color.New(color.FgRed)
lastCve := ""
for _, r := range l.Results {
if lastCve != r.CveId {
fmt.Println()
bold.Print(r.CveId)
fmt.Printf(" (%s) -- %s\n", r.Cna, r.File)
}
lastCve = r.CveId
fmt.Print(" ")
red.Printf("%s ", r.Code)
fmt.Println(r.Error.Text)
}

fmt.Printf("\nFound %d error", len(l.Results))
if len(l.Results) != 1 {
fmt.Print("s.\n")
} else {
fmt.Print(".\n")
}

case "json":
fmt.Println("{")
fmt.Printf(` "generatedAt": "%s",`+"\n", l.Timestamp.Format(time.RFC3339))
fmt.Println(` "results": [`)
for i, r := range l.Results {
fmt.Println(" {")
errorJson, _ := json.Marshal(r.Error.Text)
fmt.Printf(` "cve": "%s",`+"\n", r.CveId)
fmt.Printf(` "cna": "%s",`+"\n", r.Cna)
fmt.Printf(` "file": "%s",`+"\n", r.File)
fmt.Printf(` "ruleName": "%s",`+"\n", r.Rule.Name)
fmt.Printf(` "errorCode": "%s",`+"\n", r.Rule.Code)
fmt.Printf(` "errorPath": "%s",`+"\n", r.Error.JsonPath)
fmt.Printf(` "errorText": %s`+"\n", errorJson)
fmt.Print(" }")
if i+1 != len(l.Results) {
fmt.Print(",")
}
fmt.Println()
}
fmt.Println(" ]")
fmt.Println("}")

case "csv":
if len(l.Results) == 0 {
return
}
fmt.Println("CVE,CNA,File,RuleName,ErrorCode,ErrorText")
for _, r := range l.Results {
fmt.Printf("%s,%s,%s,%s,%s,%s,%s\n", r.CveId, r.Cna, r.File, r.Rule.Name, r.Rule.Code, r.Error.JsonPath, r.Error.Text)
}

default:
log.Fatal("ERROR: Invalid output format, must be one of: text, json, csv")
}
}
Loading

0 comments on commit b5196b6

Please sign in to comment.