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
}