mirror of
https://github.com/fleetdm/fleet
synced 2026-05-06 06:48:54 +00:00
For #28805. See scenario we want to support in the linked issue. --------- Co-authored-by: Scott Gress <scottmgress@gmail.com>
149 lines
3.9 KiB
Go
149 lines
3.9 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type OpenVEXDocument struct {
|
|
Context string `json:"context"`
|
|
Statements []Statement `json:"statements"`
|
|
Author string `json:"author"`
|
|
}
|
|
|
|
type Statement struct {
|
|
Vulnerability Vulnerability `json:"vulnerability"`
|
|
Status string `json:"status"`
|
|
StatusNotes string `json:"status_notes"`
|
|
Products []Product `json:"products"`
|
|
Justification string `json:"justification"`
|
|
Aliases []string `json:"aliases"`
|
|
ActionStatement string `json:"action_statement"`
|
|
Timestamp string `json:"timestamp"`
|
|
}
|
|
|
|
const timeFormat = "2006-01-02T15:04:05.999999Z07:00"
|
|
|
|
type Vulnerability struct {
|
|
Name string `json:"name"`
|
|
}
|
|
|
|
type Product struct {
|
|
ID string `json:"@id"`
|
|
}
|
|
|
|
func parseOpenVEX(filePath string) (*OpenVEXDocument, error) {
|
|
data, err := os.ReadFile(filePath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var vex OpenVEXDocument
|
|
err = json.Unmarshal(data, &vex)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &vex, nil
|
|
}
|
|
|
|
func generateMarkdown(vex *OpenVEXDocument) (string, error) {
|
|
var sb strings.Builder
|
|
cve := vex.Statements[0].Vulnerability.Name
|
|
for _, stmt := range vex.Statements[1:] {
|
|
if stmt.Vulnerability.Name != cve {
|
|
return "", fmt.Errorf("VEX statement does not match CVE: %s vs %s", stmt.Vulnerability.Name, cve)
|
|
}
|
|
}
|
|
sb.WriteString(fmt.Sprintf("### [%s](https://nvd.nist.gov/vuln/detail/%s)\n", cve, cve))
|
|
sort.Slice(vex.Statements, func(i, j int) bool {
|
|
ti, _ := time.Parse(timeFormat, vex.Statements[i].Timestamp)
|
|
tj, _ := time.Parse(timeFormat, vex.Statements[j].Timestamp)
|
|
return ti.After(tj)
|
|
})
|
|
multipleStatements := len(vex.Statements) > 1
|
|
for _, stmt := range vex.Statements {
|
|
if multipleStatements {
|
|
sb.WriteString("#### Statement:\n")
|
|
}
|
|
if len(stmt.Aliases) > 0 {
|
|
sb.WriteString(fmt.Sprintf("- **Aliases:** %s", strings.Join(stmt.Aliases, ",")))
|
|
}
|
|
sb.WriteString(fmt.Sprintf("- **Author:** %s\n", vex.Author))
|
|
sb.WriteString(fmt.Sprintf("- **Status:** `%s`\n", stmt.Status))
|
|
if stmt.StatusNotes != "" {
|
|
statusNotes := stmt.StatusNotes
|
|
if !strings.HasSuffix(statusNotes, ".") {
|
|
statusNotes += "."
|
|
}
|
|
sb.WriteString(fmt.Sprintf("- **Status notes:** %s\n", statusNotes))
|
|
}
|
|
sb.WriteString("- **Products:**: ")
|
|
var ids []string
|
|
for _, product := range stmt.Products {
|
|
ids = append(ids, "`"+product.ID+"`")
|
|
}
|
|
sb.WriteString(strings.Join(ids, ",") + "\n")
|
|
if stmt.Justification != "" {
|
|
sb.WriteString(fmt.Sprintf("- **Justification:** `%s`\n", stmt.Justification))
|
|
}
|
|
if stmt.ActionStatement != "" {
|
|
sb.WriteString(fmt.Sprintf("- **Action statement:** `%s`\n", stmt.ActionStatement))
|
|
}
|
|
if stmt.Timestamp != "" {
|
|
t, err := time.Parse(timeFormat, stmt.Timestamp)
|
|
if err != nil {
|
|
return "", fmt.Errorf("parsing timestamp %s for %s: %s", stmt.Timestamp, stmt.Vulnerability.Name, err)
|
|
}
|
|
sb.WriteString(fmt.Sprintf("- **Timestamp:** %s\n", t.Format(time.DateTime)))
|
|
}
|
|
sb.WriteString("\n")
|
|
}
|
|
|
|
return sb.String(), nil
|
|
}
|
|
|
|
func outputMarkdown(vexPath string) error {
|
|
vex, err := parseOpenVEX(vexPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
md, err := generateMarkdown(vex)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Print(md)
|
|
return nil
|
|
}
|
|
|
|
func main() {
|
|
if len(os.Args) < 2 {
|
|
fmt.Println("Usage: go run main.go /path/to/directory/with/vex.json/files/")
|
|
os.Exit(1)
|
|
}
|
|
|
|
vexPath := os.Args[1]
|
|
vexPaths, err := filepath.Glob(filepath.Join(vexPath, "*.vex.json"))
|
|
if err != nil {
|
|
fmt.Printf("Error processing directory %q: %v\n", vexPath, err)
|
|
os.Exit(1)
|
|
}
|
|
if len(vexPaths) == 0 {
|
|
fmt.Printf("No vulnerabilities tracked at the moment.\n\n")
|
|
return
|
|
}
|
|
|
|
sort.Slice(vexPaths, func(i, j int) bool {
|
|
return vexPaths[i] > vexPaths[j]
|
|
})
|
|
for _, vexPath := range vexPaths {
|
|
if err := outputMarkdown(vexPath); err != nil {
|
|
fmt.Printf("Error parsing OpenVEX file %q: %s\n", vexPath, err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
}
|