git/orchard/main
tui/internal/plan/detail.go
199 lines6 KB last change raw · history
package plan

import (
	"context"
	"encoding/json"
	"fmt"
)

// Advisory is one security advisory from arch-audit.
type Advisory struct {
	ID       string   `json:"id"`
	Severity string   `json:"severity"`
	Status   string   `json:"status"`
	Kind     string   `json:"kind"`
	Fixed    string   `json:"fixed"`
	URL      string   `json:"url"`
	Packages []string `json:"packages"`
	Issues   []string `json:"issues"`
}

// HasFix reports whether an update would resolve the advisory.
func (a Advisory) HasFix() bool { return a.Fixed != "" }

// DiffLine is one row of a diff. Kind is ctx, add, del or gap.
type DiffLine struct {
	Kind string `json:"kind"`
	Old  *int   `json:"old"`
	New  *int   `json:"new"`
	Text string `json:"text"`
}

// Diff is a structured diff plus its counts.
type Diff struct {
	Lines     []DiffLine `json:"lines"`
	Added     int        `json:"added"`
	Removed   int        `json:"removed"`
	Truncated bool       `json:"truncated"`
}

// Finding is one audit result.
type Finding struct {
	Rule     string `json:"rule"`
	Severity string `json:"severity"`
	Title    string `json:"title"`
	Evidence string `json:"evidence"`
	Where    string `json:"where"`
}

// Glyph mirrors the severity marker the rest of the tool uses.
func (f Finding) Glyph() string {
	switch f.Severity {
	case "BLOCK":
		return "!!"
	case "LOOK":
		return "!"
	default:
		return "·"
	}
}

// Role maps a severity onto a palette role name.
func (f Finding) Role() string {
	switch f.Severity {
	case "BLOCK":
		return "alert"
	case "LOOK":
		return "warn"
	default:
		return "muted"
	}
}

// Meta is what the AUR knows about a package.
type Meta struct {
	Maintainer     string   `json:"maintainer"`
	Orphaned       bool     `json:"orphaned"`
	OutOfDate      *int64   `json:"out_of_date"`
	Votes          int      `json:"votes"`
	Popularity     float64  `json:"popularity"`
	FirstSubmitted int64    `json:"first_submitted"`
	LastModified   int64    `json:"last_modified"`
	URL            string   `json:"url"`
	PackageBase    string   `json:"package_base"`
	Depends        []string `json:"depends"`
	MakeDepends    []string `json:"make_depends"`
}

// Detail is the lazy drill-down payload for one package.
type Detail struct {
	Package              string     `json:"package"`
	Kind                 string     `json:"kind"`
	Installed            string     `json:"installed"`
	Available            string     `json:"available"`
	BaselineCommit       string     `json:"baseline_commit"`
	BaselineHow          string     `json:"baseline_how"`
	BaselineNote         string     `json:"baseline_note"`
	CurrentCommit        string     `json:"current_commit"`
	CurrentCommitSummary string     `json:"current_commit_summary"`
	CurrentCommitWhen    string     `json:"current_commit_when"`
	Diff                 Diff       `json:"diff"`
	Findings             []Finding  `json:"findings"`
	Verdict              string     `json:"verdict"`
	Meta                 Meta       `json:"meta"`
	Advisories           []Advisory `json:"advisories"`
	Pkgbuild             []string   `json:"pkgbuild"`
	Ignored              bool       `json:"ignored"`
	//: the available version is older than the installed one
	Downgrade bool `json:"downgrade"`
}

// BaselineLabel says what the diff was compared against, in the user's terms.
func (d *Detail) BaselineLabel() string {
	switch d.BaselineHow {
	case "recorded":
		return "vs last built (" + short(d.BaselineCommit) + ")"
	case "version":
		return fmt.Sprintf("vs revision matching installed %s (%s)",
			d.Installed, short(d.BaselineCommit))
	default:
		return "no baseline"
	}
}

func short(sha string) string {
	if len(sha) > 8 {
		return sha[:8]
	}
	return sha
}

// LoadDetail fetches the drill-down for one package.
func (b Backend) LoadDetail(ctx context.Context, pkg string) (*Detail, error) {
	args := append([]string{"show", pkg, "--json"}, b.Args...)
	out, err := b.Run(ctx, args...)
	if err != nil {
		return nil, err
	}
	var d Detail
	if err := json.Unmarshal(out, &d); err != nil {
		return nil, fmt.Errorf("could not parse detail for %s: %w", pkg, err)
	}
	return &d, nil
}

// AdvisoryDetail is the security tracker's own record for one advisory.
type AdvisoryDetail struct {
	ID         string   `json:"id"`
	Available  bool     `json:"available"`
	Reason     string   `json:"reason"`
	Packages   []string `json:"packages"`
	Status     string   `json:"status"`
	Severity   string   `json:"severity"`
	Kind       string   `json:"kind"`
	Affected   string   `json:"affected"`
	Fixed      string   `json:"fixed"`
	Ticket     string   `json:"ticket"`
	Issues     []string `json:"issues"`
	Advisories []string `json:"advisories"`
	References []string `json:"references"`
	Notes      string   `json:"notes"`
	URL        string   `json:"url"`
}

// SecurityReport is a fresh arch-audit run.
type SecurityReport struct {
	Available  bool       `json:"available"`
	Reason     string     `json:"reason"`
	Total      int        `json:"total"`
	Fixable    int        `json:"fixable"`
	Unfixable  int        `json:"unfixable"`
	Advisories []Advisory `json:"advisories"`
}

// LoadAdvisory fetches one advisory from the tracker.
func (b Backend) LoadAdvisory(ctx context.Context, id string) (*AdvisoryDetail, error) {
	out, err := b.Run(ctx, "advisory", id, "--json")
	if err != nil {
		return nil, err
	}
	var detail AdvisoryDetail
	if err := json.Unmarshal(out, &detail); err != nil {
		return nil, fmt.Errorf("could not parse the advisory for %s: %w", id, err)
	}
	return &detail, nil
}

// RunSecurity runs arch-audit again, now.
func (b Backend) RunSecurity(ctx context.Context) (*SecurityReport, error) {
	out, err := b.Run(ctx, "security", "--json")
	if err != nil {
		return nil, err
	}
	var report SecurityReport
	if err := json.Unmarshal(out, &report); err != nil {
		return nil, fmt.Errorf("could not parse the security run: %w", err)
	}
	return &report, nil
}