Commit d5c2bce4b976
Changed files (6)
tools
daily-plan
cmd
daily-plan
internal
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 = {