Commit 3918251f4747
Changed files (3)
tools
daily-plan
cmd
daily-plan
internal
display
github
tools/daily-plan/cmd/daily-plan/main.go
@@ -957,15 +957,37 @@ func cmdReview(ctx context.Context, cfg *config.Config, period string) error {
}
if len(advisories) > 0 {
- fmt.Printf("## Security Advisories (%d)\n\n", len(advisories))
+ var involved, triaged []github.SecurityAdvisory
for _, a := range advisories {
+ if a.Role == "triaged" {
+ triaged = append(triaged, a)
+ } else {
+ involved = append(involved, a)
+ }
+ }
+ fmt.Printf("## Security Advisories (%d — %d credited/authored, %d triaged)\n\n",
+ len(advisories), len(involved), len(triaged))
+ printAdvisory := func(a github.SecurityAdvisory) {
cve := ""
if a.CVEID != "" {
cve = fmt.Sprintf(" (%s)", a.CVEID)
}
- fmt.Printf("- [%s] **%s** %s%s — %s\n", a.State, a.GHSAID, a.Summary, cve, a.Repo)
+ fmt.Printf("- [%s] **%s** %s%s — %s _(%s)_\n", a.State, a.GHSAID, a.Summary, cve, a.Repo, a.Role)
+ }
+ if len(involved) > 0 {
+ fmt.Print("### Credited / Authored\n\n")
+ for _, a := range involved {
+ printAdvisory(a)
+ }
+ fmt.Println()
+ }
+ if len(triaged) > 0 {
+ fmt.Print("### Triaged (VMT review queue)\n\n")
+ for _, a := range triaged {
+ printAdvisory(a)
+ }
+ fmt.Println()
}
- fmt.Println()
}
if len(aiItems) > 0 {
tools/daily-plan/internal/display/display.go
@@ -166,8 +166,8 @@ func SecurityAdvisories(advisories []github.SecurityAdvisory) {
if len(summary) > 60 {
summary = summary[:57] + "..."
}
- fmt.Printf(" %s⚠ %-20s%s %-60s %s[%s, %s]%s\n",
- sevColor, ghsa, reset, summary, dim, a.Severity, a.Repo, reset)
+ fmt.Printf(" %s⚠ %-20s%s %-60s %s[%s, %s, %s]%s\n",
+ sevColor, ghsa, reset, summary, dim, a.Severity, a.Repo, a.Role, reset)
}
}
tools/daily-plan/internal/github/github.go
@@ -434,10 +434,10 @@ type SecurityAdvisory struct {
Severity string `json:"severity"`
State string `json:"state"`
HTMLURL string `json:"html_url"`
- Repo string `json:"-"` // filled in by caller
+ Repo string `json:"repo"` // filled in by caller; persisted for cache
CreatedAt time.Time `json:"created_at"`
- Credits []string `json:"-"` // credit logins, filled from raw response
- Role string `json:"-"` // "credited", "collaborator", "author"
+ Credits []string `json:"credits"` // credit logins, filled from raw response
+ Role string `json:"role"` // "author", "credited", or "triaged"
}
// FetchSecurityAdvisories fetches open/triage security advisories for specific repos.
@@ -462,14 +462,17 @@ func FetchSecurityAdvisories(ctx context.Context, repos []string, states []strin
// advisoryRaw is the raw JSON shape from the GitHub Security Advisories API,
// including credits and collaborating_users for involvement filtering.
type advisoryRaw struct {
- GHSAID string `json:"ghsa_id"`
- CVEID string `json:"cve_id"`
- Summary string `json:"summary"`
- Severity string `json:"severity"`
- State string `json:"state"`
- HTMLURL string `json:"html_url"`
- CreatedAt time.Time `json:"created_at"`
- Author struct {
+ GHSAID string `json:"ghsa_id"`
+ CVEID string `json:"cve_id"`
+ Summary string `json:"summary"`
+ Severity string `json:"severity"`
+ State string `json:"state"`
+ HTMLURL string `json:"html_url"`
+ CreatedAt time.Time `json:"created_at"`
+ PublishedAt *time.Time `json:"published_at"`
+ ClosedAt *time.Time `json:"closed_at"`
+ UpdatedAt *time.Time `json:"updated_at"`
+ Author struct {
Login string `json:"login"`
} `json:"author"`
Credits []struct {
@@ -533,6 +536,31 @@ func userInvolved(raw advisoryRaw, username string) string {
return ""
}
+// userInvolvedOrTriaged returns the user's role for an advisory. If the user is
+// not the author or credited, it returns "triaged" — because the configured
+// security repos are VMT repos, every advisory in them is reviewed as part of
+// the security queue even when the API exposes no per-user comment trail.
+func userInvolvedOrTriaged(raw advisoryRaw, username string) string {
+ if role := userInvolved(raw, username); role != "" {
+ return role
+ }
+ return "triaged"
+}
+
+// advisoryActiveInWindow reports whether an advisory was created, published,
+// closed, or updated on or after since. The previous logic only checked
+// created_at, which dropped advisories opened before the period but triaged,
+// published, or closed within it.
+func advisoryActiveInWindow(raw advisoryRaw, since time.Time) bool {
+ candidates := []*time.Time{&raw.CreatedAt, raw.PublishedAt, raw.ClosedAt, raw.UpdatedAt}
+ for _, t := range candidates {
+ if t != nil && !t.IsZero() && !t.Before(since) {
+ return true
+ }
+ }
+ return false
+}
+
// DependabotAlert represents a Dependabot security alert.
type DependabotAlert struct {
Number int `json:"number"`
@@ -643,15 +671,22 @@ func fetchByMonth(ctx context.Context, since time.Time, queryFn func(start, end
return all, nil
}
-// FetchSecurityAdvisoriesSince fetches security advisories created since the
-// given date where the specified user is involved (credited, collaborating, or author).
+// FetchSecurityAdvisoriesSince fetches security advisories that were active
+// (created, published, closed, or updated) since the given date across the
+// configured repos. Each advisory is tagged with the user's Role:
+// - "author" = the user opened the advisory
+// - "credited" = the user is in the credits list
+// - "triaged" = neither, but the repo is a VMT repo so the advisory was
+// reviewed as part of the security queue
+//
+// Uses --paginate with per_page=100 to avoid silently truncating high-volume
+// repos (the previous per_page=30 cap dropped results for tektoncd/pipeline).
func FetchSecurityAdvisoriesSince(ctx context.Context, repos []string, since time.Time, username string) ([]SecurityAdvisory, error) {
var all []SecurityAdvisory
for _, repo := range repos {
for _, state := range []string{"published", "closed", "draft", "triage"} {
- // Fetch raw to check involvement
- endpoint := fmt.Sprintf("repos/%s/security-advisories?state=%s&per_page=30", repo, state)
- cmd := exec.CommandContext(ctx, "gh", "api", endpoint)
+ endpoint := fmt.Sprintf("repos/%s/security-advisories?state=%s&per_page=100", repo, state)
+ cmd := exec.CommandContext(ctx, "gh", "api", "--paginate", "--slurp", endpoint)
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
@@ -659,35 +694,40 @@ func FetchSecurityAdvisoriesSince(ctx context.Context, repos []string, since tim
continue
}
- var raw []advisoryRaw
- if err := json.Unmarshal(stdout.Bytes(), &raw); err != nil {
- continue
+ // --slurp wraps paginated pages in an outer array: [[...],[...]].
+ var pages [][]advisoryRaw
+ if err := json.Unmarshal(stdout.Bytes(), &pages); err != nil {
+ // Fall back to a single un-slurped array.
+ var single []advisoryRaw
+ if err2 := json.Unmarshal(stdout.Bytes(), &single); err2 != nil {
+ continue
+ }
+ pages = [][]advisoryRaw{single}
}
- for _, r := range raw {
- if r.CreatedAt.Before(since) {
- continue
+ for _, page := range pages {
+ for _, r := range page {
+ if !advisoryActiveInWindow(r, since) {
+ continue
+ }
+ role := userInvolvedOrTriaged(r, username)
+ var credits []string
+ for _, c := range r.Credits {
+ credits = append(credits, c.Login)
+ }
+ all = append(all, SecurityAdvisory{
+ GHSAID: r.GHSAID,
+ CVEID: r.CVEID,
+ Summary: r.Summary,
+ Severity: r.Severity,
+ State: r.State,
+ HTMLURL: r.HTMLURL,
+ CreatedAt: r.CreatedAt,
+ Repo: repo,
+ Credits: credits,
+ Role: role,
+ })
}
- role := userInvolved(r, username)
- if role == "" {
- continue
- }
- var credits []string
- for _, c := range r.Credits {
- credits = append(credits, c.Login)
- }
- all = append(all, SecurityAdvisory{
- GHSAID: r.GHSAID,
- CVEID: r.CVEID,
- Summary: r.Summary,
- Severity: r.Severity,
- State: r.State,
- HTMLURL: r.HTMLURL,
- CreatedAt: r.CreatedAt,
- Repo: repo,
- Credits: credits,
- Role: role,
- })
}
}
}