Commit d5c2bce4b976

Vincent Demeester <vincent@sbr.pm>
2026-03-26 16:32:37
feat(daily-plan): add GitHub security advisories and dependabot alerts
Inbox now shows: - GitHub Security Advisories (triage/draft state) from tektoncd repos Uses /repos/{owner}/{repo}/security-advisories API - Dependabot Alerts (critical/high severity) at org level Uses /orgs/{org}/dependabot/alerts API Security advisories appear first in inbox, before Jira CVEs. Both have --json support and Emacs rendering with clickable links. SecurityRepos config limits advisory queries to repos where you're a maintainer (8 tektoncd repos), avoiding excessive API calls.
1 parent 64b34e3
Changed files (6)
tools
daily-plan
tools/daily-plan/cmd/daily-plan/main.go
@@ -84,23 +84,44 @@ func run(args []string) error {
 
 // JSON output types for Emacs integration.
 type jsonShow struct {
-	Date           string       `json:"date"`
-	Agenda         []jsonOrg    `json:"agenda"`
-	JiraInProgress []jsonJira   `json:"jira_in_progress"`
-	JiraBacklog    []jsonJira   `json:"jira_backlog"`
-	GHIssues       []jsonGH     `json:"github_issues"`
-	GHReviews      []jsonGH     `json:"github_reviews"`
-	GHPRs          []jsonGH     `json:"github_prs"`
+	Date           string     `json:"date"`
+	Agenda         []jsonOrg  `json:"agenda"`
+	JiraInProgress []jsonJira `json:"jira_in_progress"`
+	JiraBacklog    []jsonJira `json:"jira_backlog"`
+	GHIssues       []jsonGH   `json:"github_issues"`
+	GHReviews      []jsonGH   `json:"github_reviews"`
+	GHPRs          []jsonGH   `json:"github_prs"`
 }
 
 type jsonInbox struct {
-	Since      string   `json:"since"`
-	CVEs       []jsonJira `json:"cves"`
-	CVETotal   int        `json:"cve_total"`
-	Updated    []jsonJira `json:"jira_updated"`
-	NewIssues  []jsonGH   `json:"github_new_issues"`
-	NewPRs     []jsonGH   `json:"github_new_prs"`
-	Reviews    []jsonGH   `json:"github_reviews"`
+	Since              string           `json:"since"`
+	CVEs               []jsonJira       `json:"cves"`
+	CVETotal           int              `json:"cve_total"`
+	Updated            []jsonJira       `json:"jira_updated"`
+	SecurityAdvisories []jsonAdvisory   `json:"github_security_advisories"`
+	DependabotAlerts   []jsonDependabot `json:"github_dependabot_alerts"`
+	NewIssues          []jsonGH         `json:"github_new_issues"`
+	NewPRs             []jsonGH         `json:"github_new_prs"`
+	Reviews            []jsonGH         `json:"github_reviews"`
+}
+
+type jsonAdvisory struct {
+	GHSAID   string `json:"ghsa_id"`
+	CVEID    string `json:"cve_id,omitempty"`
+	Summary  string `json:"summary"`
+	Severity string `json:"severity"`
+	State    string `json:"state"`
+	Repo     string `json:"repo"`
+	URL      string `json:"url"`
+}
+
+type jsonDependabot struct {
+	CVE      string `json:"cve,omitempty"`
+	Summary  string `json:"summary"`
+	Severity string `json:"severity"`
+	Package  string `json:"package"`
+	Repo     string `json:"repo"`
+	URL      string `json:"url"`
 }
 
 type jsonOrg struct {
@@ -218,6 +239,8 @@ func cmdInbox(ctx context.Context, cfg *config.Config, sinceStr string) error {
 	cves, _ := jira.FetchSecuritySince(ctx, since)
 	grouped, total := jira.GroupByCVE(cves)
 	updated, _ := jira.FetchUpdatedSince(ctx, since)
+	advisories, _ := github.FetchSecurityAdvisories(ctx, cfg.GitHub.SecurityRepos, []string{"triage", "draft"})
+	dependabot, _ := github.FetchDependabotAlerts(ctx, cfg.GitHub.Owners, "critical,high")
 	newIssues, _ := github.FetchNewIssuesSince(ctx, since, cfg.GitHub.Owners)
 	newPRs, _ := github.FetchNewPRsSince(ctx, since, cfg.GitHub.Owners)
 	newPRs = github.FilterBots(newPRs, cfg.GitHub.BotFilters)
@@ -226,19 +249,41 @@ func cmdInbox(ctx context.Context, cfg *config.Config, sinceStr string) error {
 
 	if outputJSON {
 		updateLastCheck(cfg)
+		ja := make([]jsonAdvisory, 0, len(advisories))
+		for _, a := range advisories {
+			ja = append(ja, jsonAdvisory{
+				GHSAID: a.GHSAID, CVEID: a.CVEID, Summary: a.Summary,
+				Severity: a.Severity, State: a.State, Repo: a.Repo, URL: a.HTMLURL,
+			})
+		}
+		jd := make([]jsonDependabot, 0, len(dependabot))
+		for _, d := range dependabot {
+			jd = append(jd, jsonDependabot{
+				CVE: d.CVE, Summary: d.Summary, Severity: d.Severity,
+				Package: d.Package, Repo: d.Repo, URL: d.HTMLURL,
+			})
+		}
 		return emitJSON(jsonInbox{
-			Since:     since.Format("2006-01-02"),
-			CVEs:      jiraToJSON(grouped, cfg.Jira.BaseURL),
-			CVETotal:  total,
-			Updated:   jiraToJSON(updated, cfg.Jira.BaseURL),
-			NewIssues: ghToJSON(newIssues),
-			NewPRs:    ghToJSON(newPRs),
-			Reviews:   ghToJSON(reviews),
+			Since:              since.Format("2006-01-02"),
+			CVEs:               jiraToJSON(grouped, cfg.Jira.BaseURL),
+			CVETotal:           total,
+			Updated:            jiraToJSON(updated, cfg.Jira.BaseURL),
+			SecurityAdvisories: ja,
+			DependabotAlerts:   jd,
+			NewIssues:          ghToJSON(newIssues),
+			NewPRs:             ghToJSON(newPRs),
+			Reviews:            ghToJSON(reviews),
 		})
 	}
 
 	display.Header(fmt.Sprintf("New/Updated Since %s", since.Format("2006-01-02")))
 
+	display.SubHeader("GitHub — Security Advisories (triage/draft)")
+	display.SecurityAdvisories(advisories)
+
+	display.SubHeader("GitHub — Dependabot Alerts (critical/high)")
+	display.DependabotAlerts(dependabot)
+
 	display.SubHeader("Jira — New CVEs / Security Issues")
 	display.CVEIssues(grouped, total)
 
@@ -384,13 +429,13 @@ func cmdPick(_ context.Context, cfg *config.Config) error {
 }
 
 type jsonWeekly struct {
-	Week          string     `json:"week"`
-	NextMonday    string     `json:"next_monday"`
-	JiraCompleted []jsonJira `json:"jira_completed"`
-	GHMerged      []jsonGH   `json:"github_merged"`
+	Week           string     `json:"week"`
+	NextMonday     string     `json:"next_monday"`
+	JiraCompleted  []jsonJira `json:"jira_completed"`
+	GHMerged       []jsonGH   `json:"github_merged"`
 	JiraInProgress []jsonJira `json:"jira_in_progress"`
-	JiraBacklog   []jsonJira `json:"jira_backlog"`
-	GHIssues      []jsonGH   `json:"github_issues"`
+	JiraBacklog    []jsonJira `json:"jira_backlog"`
+	GHIssues       []jsonGH   `json:"github_issues"`
 }
 
 func cmdWeekly(ctx context.Context, cfg *config.Config) error {
tools/daily-plan/internal/config/config.go
@@ -32,6 +32,9 @@ type JiraConfig struct {
 type GitHubConfig struct {
 	Username string   // GitHub username
 	Owners   []string // GitHub orgs/users to track
+	// SecurityRepos are repos to check for security advisories (GHSAs).
+	// These are repos where you're a maintainer with security advisory access.
+	SecurityRepos []string
 	// BotFilters are author patterns to filter out from inbox
 	BotFilters []string
 }
@@ -54,6 +57,16 @@ func DefaultConfig() *Config {
 		GitHub: GitHubConfig{
 			Username: "vdemeester",
 			Owners:   []string{"tektoncd", "openshift-pipelines"},
+			SecurityRepos: []string{
+				"tektoncd/pipeline",
+				"tektoncd/cli",
+				"tektoncd/triggers",
+				"tektoncd/chains",
+				"tektoncd/operator",
+				"tektoncd/results",
+				"tektoncd/dashboard",
+				"tektoncd/pipelines-as-code",
+			},
 			BotFilters: []string{
 				"openshift-pipelines-bot",
 				"red-hat-konflux",
tools/daily-plan/internal/display/display.go
@@ -141,6 +141,62 @@ func GitHubItems(items []github.Item, style string) {
 	}
 }
 
+// SecurityAdvisories prints GitHub security advisories.
+func SecurityAdvisories(advisories []github.SecurityAdvisory) {
+	if len(advisories) == 0 {
+		fmt.Printf("  %s(none)%s\n", dim, reset)
+		return
+	}
+	for _, a := range advisories {
+		sevColor := yellow
+		switch a.Severity {
+		case "critical":
+			sevColor = red
+		case "high":
+			sevColor = red
+		case "low":
+			sevColor = dim
+		}
+		ghsa := a.GHSAID
+		if a.CVEID != "" {
+			ghsa = a.CVEID
+		}
+		summary := a.Summary
+		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)
+	}
+}
+
+// DependabotAlerts prints dependabot alerts.
+func DependabotAlerts(alerts []github.DependabotAlert) {
+	if len(alerts) == 0 {
+		fmt.Printf("  %s(none)%s\n", dim, reset)
+		return
+	}
+	for _, a := range alerts {
+		sevColor := yellow
+		switch a.Severity {
+		case "critical", "high":
+			sevColor = red
+		case "low":
+			sevColor = dim
+		}
+		id := a.CVE
+		if id == "" {
+			id = fmt.Sprintf("#%d", a.Number)
+		}
+		summary := a.Summary
+		if len(summary) > 55 {
+			summary = summary[:52] + "..."
+		}
+		fmt.Printf("  %s⚠ %-20s%s %-55s %s[%s, %s, %s]%s\n",
+			sevColor, id, reset, summary, dim, a.Severity, a.Package, a.Repo, reset)
+	}
+}
+
 // Hint prints a dim hint line.
 func Hint(msg string) {
 	fmt.Printf("\n%s%s%s\n", dim, msg, reset)
tools/daily-plan/internal/github/github.go
@@ -40,9 +40,9 @@ type ghSearchResult struct {
 	Repository struct {
 		NameWithOwner string `json:"nameWithOwner"`
 	} `json:"repository"`
-	Number    int    `json:"number"`
-	Title     string `json:"title"`
-	Author    struct {
+	Number int    `json:"number"`
+	Title  string `json:"title"`
+	Author struct {
 		Login string `json:"login"`
 	} `json:"author"`
 	CreatedAt time.Time  `json:"createdAt"`
@@ -161,6 +161,124 @@ func FilterBots(items []Item, botPatterns []string) []Item {
 	return filtered
 }
 
+// SecurityAdvisory represents a GitHub security advisory (GHSA).
+type SecurityAdvisory 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"`
+	Repo      string    `json:"-"` // filled in by caller
+	CreatedAt time.Time `json:"created_at"`
+}
+
+// FetchSecurityAdvisories fetches open/triage security advisories for specific repos.
+// Only queries repos explicitly listed to avoid excessive API calls.
+func FetchSecurityAdvisories(ctx context.Context, repos []string, states []string) ([]SecurityAdvisory, error) {
+	var all []SecurityAdvisory
+	for _, repo := range repos {
+		for _, state := range states {
+			advisories, err := fetchRepoAdvisories(ctx, repo, state)
+			if err != nil {
+				continue
+			}
+			for i := range advisories {
+				advisories[i].Repo = repo
+			}
+			all = append(all, advisories...)
+		}
+	}
+	return all, nil
+}
+
+func fetchRepoAdvisories(ctx context.Context, repo, state string) ([]SecurityAdvisory, error) {
+	endpoint := fmt.Sprintf("repos/%s/security-advisories?state=%s&per_page=30", repo, state)
+	cmd := exec.CommandContext(ctx, "gh", "api", endpoint)
+	var stdout, stderr bytes.Buffer
+	cmd.Stdout = &stdout
+	cmd.Stderr = &stderr
+	if err := cmd.Run(); err != nil {
+		return nil, fmt.Errorf("gh api error: %s", stderr.String())
+	}
+
+	var advisories []SecurityAdvisory
+	if err := json.Unmarshal(stdout.Bytes(), &advisories); err != nil {
+		return nil, err
+	}
+	return advisories, nil
+}
+
+// DependabotAlert represents a Dependabot security alert.
+type DependabotAlert struct {
+	Number    int       `json:"number"`
+	State     string    `json:"state"`
+	Repo      string    `json:"-"` // filled in from response
+	HTMLURL   string    `json:"html_url"`
+	CreatedAt time.Time `json:"created_at"`
+	Severity  string    `json:"-"` // extracted from nested field
+	CVE       string    `json:"-"` // extracted from nested field
+	Summary   string    `json:"-"` // extracted from nested field
+	Package   string    `json:"-"` // extracted from nested field
+}
+
+type dependabotAlertRaw struct {
+	Number     int       `json:"number"`
+	State      string    `json:"state"`
+	HTMLURL    string    `json:"html_url"`
+	CreatedAt  time.Time `json:"created_at"`
+	Repository struct {
+		FullName string `json:"full_name"`
+	} `json:"repository"`
+	SecurityAdvisory struct {
+		CVEID    string `json:"cve_id"`
+		Summary  string `json:"summary"`
+		Severity string `json:"severity"`
+	} `json:"security_advisory"`
+	Dependency struct {
+		Package struct {
+			Name string `json:"name"`
+		} `json:"package"`
+	} `json:"dependency"`
+}
+
+// FetchDependabotAlerts fetches open dependabot alerts at org level.
+func FetchDependabotAlerts(ctx context.Context, owners []string, severity string) ([]DependabotAlert, error) {
+	var all []DependabotAlert
+	for _, org := range owners {
+		endpoint := fmt.Sprintf("orgs/%s/dependabot/alerts?state=open&sort=created&direction=desc&per_page=30", org)
+		if severity != "" {
+			endpoint += "&severity=" + severity
+		}
+		cmd := exec.CommandContext(ctx, "gh", "api", endpoint)
+		var stdout, stderr bytes.Buffer
+		cmd.Stdout = &stdout
+		cmd.Stderr = &stderr
+		if err := cmd.Run(); err != nil {
+			continue // Skip orgs we don't have access to
+		}
+
+		var raw []dependabotAlertRaw
+		if err := json.Unmarshal(stdout.Bytes(), &raw); err != nil {
+			continue
+		}
+		for _, r := range raw {
+			all = append(all, DependabotAlert{
+				Number:    r.Number,
+				State:     r.State,
+				Repo:      r.Repository.FullName,
+				HTMLURL:   r.HTMLURL,
+				CreatedAt: r.CreatedAt,
+				Severity:  r.SecurityAdvisory.Severity,
+				CVE:       r.SecurityAdvisory.CVEID,
+				Summary:   r.SecurityAdvisory.Summary,
+				Package:   r.Dependency.Package.Name,
+			})
+		}
+	}
+	return all, nil
+}
+
 func ownerFlags(owners []string) []string {
 	var flags []string
 	for _, owner := range owners {
tools/daily-plan/daily-plan.el
@@ -124,12 +124,42 @@
     (insert "\n** GitHub — Your Open PRs\n")
     (daily-plan--insert-items .github_prs #'daily-plan--insert-gh-item nil)))
 
+(defun daily-plan--insert-advisory (item)
+  "Insert a GitHub security advisory ITEM."
+  (let-alist item
+    (let ((start (point))
+          (id (or .cve_id .ghsa_id)))
+      (insert (format "- [[%s][%s]] %s =%s= /%s/"
+                      .url id .summary .severity .repo))
+      (insert "\n")
+      (put-text-property start (point) 'daily-plan-key id)
+      (put-text-property start (point) 'daily-plan-url .url)
+      (put-text-property start (point) 'daily-plan-type "advisory"))))
+
+(defun daily-plan--insert-dependabot (item)
+  "Insert a Dependabot alert ITEM."
+  (let-alist item
+    (let ((start (point))
+          (id (or .cve "dependabot")))
+      (insert (format "- [[%s][%s]] %s =%s= ~%s~ /%s/"
+                      .url id .summary .severity .package .repo))
+      (insert "\n")
+      (put-text-property start (point) 'daily-plan-key id)
+      (put-text-property start (point) 'daily-plan-url .url)
+      (put-text-property start (point) 'daily-plan-type "dependabot"))))
+
 (defun daily-plan--render-inbox (data)
   "Render inbox DATA into current buffer."
   (let-alist data
     (insert (format "* Inbox — Since %s\n\n" .since))
 
-    (insert "** CVEs / Security Issues  :security:\n")
+    (insert "** GitHub — Security Advisories (triage/draft)  :security:\n")
+    (daily-plan--insert-items .github_security_advisories #'daily-plan--insert-advisory nil)
+
+    (insert "\n** GitHub — Dependabot Alerts (critical/high)  :security:\n")
+    (daily-plan--insert-items .github_dependabot_alerts #'daily-plan--insert-dependabot nil)
+
+    (insert "\n** Jira — CVEs / Security Issues  :security:\n")
     (daily-plan--insert-items .cves #'daily-plan--insert-jira-item nil)
     (when (and .cve_total (> .cve_total (length .cves)))
       (insert (format "  /(%d total across images)/\n" .cve_total)))
tools/daily-plan/default.nix
@@ -20,12 +20,7 @@ buildGoModule {
   # jira CLI is NOT bundled — expects the host's wrapper (injects token via passage)
   postInstall = ''
     wrapProgram $out/bin/daily-plan \
-      --prefix PATH : ${
-        lib.makeBinPath (
-          [ gh ]
-          ++ lib.optional (jayrat != null) jayrat
-        )
-      }
+      --prefix PATH : ${lib.makeBinPath ([ gh ] ++ lib.optional (jayrat != null) jayrat)}
   '';
 
   meta = {