From bf1148246412db1544f4bf7c3c42c7e5e82af572 Mon Sep 17 00:00:00 2001 From: rpunia Date: Thu, 6 Nov 2025 23:20:04 -0800 Subject: [PATCH 1/5] create single issue for a given minor version --- securityscanutils/commands/scan_repo.go | 8 ++++--- .../issuewriter/github_writer.go | 23 ++++++++++++++++++- securityscanutils/securityscan.go | 11 ++++++++- 3 files changed, 37 insertions(+), 5 deletions(-) diff --git a/securityscanutils/commands/scan_repo.go b/securityscanutils/commands/scan_repo.go index 42aafb11..9e48f5f0 100644 --- a/securityscanutils/commands/scan_repo.go +++ b/securityscanutils/commands/scan_repo.go @@ -42,6 +42,7 @@ type scanRepoOptions struct { // action to take when a vulnerability is discovered. supported actions are: // none (default): do nothing when a vulnerability is discovered // github-issue-latest (preferred): create a github issue only for the latest patch version of each minor version, when a vulnerability is discovered + // github-issue-minor: create/update a single github issue per minor (e.g. 2.0.x) // github-issue-all: create a github issue for every version where a vulnerability is discovered // output-locally: create a file in the generated output dir containing the final Markdown for each repo / version vulnerabilityAction string @@ -57,7 +58,7 @@ func (m *scanRepoOptions) addToFlags(flags *pflag.FlagSet) { flags.StringVarP(&m.githubRepository, "github-repo", "g", "", "github repository to scan") flags.StringVarP(&m.imageRepository, "image-repo", "r", securityscanutils.QuayRepository, "image repository to scan") - flags.StringVarP(&m.vulnerabilityAction, "vulnerability-action", "a", "none", "action to take when a vulnerability is discovered {none, github-issue-all, github-issue-latest, output-locally}") + flags.StringVarP(&m.vulnerabilityAction, "vulnerability-action", "a", "none", "action to take when a vulnerability is discovered {none, github-issue-all, github-issue-latest, github-issue-minor, output-locally}") flags.StringVarP(&m.releaseVersionConstraint, "release-constraint", "c", "", "version constraint for releases to scan") flags.BoolVar(&m.enablePreRelease, "enable-pre-release", false, "enable pre-release versions to be scanned") @@ -97,8 +98,9 @@ func doScanRepo(ctx context.Context, opts *scanRepoOptions) error { OutputResultLocally: opts.vulnerabilityAction == "output-locally", CreateGithubIssuePerVersion: opts.vulnerabilityAction == "github-issue-all", CreateGithubIssueForLatestPatchVersion: opts.vulnerabilityAction == "github-issue-latest", - AdditionalContext: additionalContext, - EnablePreRelease: opts.enablePreRelease, + CreateGithubIssueForMinorLatestPatchVersion: opts.vulnerabilityAction == "github-issue-minor", + AdditionalContext: additionalContext, + EnablePreRelease: opts.enablePreRelease, }, }, }, diff --git a/securityscanutils/issuewriter/github_writer.go b/securityscanutils/issuewriter/github_writer.go index dfc20911..7917ea1e 100644 --- a/securityscanutils/issuewriter/github_writer.go +++ b/securityscanutils/issuewriter/github_writer.go @@ -38,6 +38,10 @@ type GithubIssueWriter struct { // A local cache of all existing GitHub issues // Used to ensure that we are updating existing issues that were created by previous scans allGithubIssues []*github.Issue + + // If true, the issue title will be created per minor (e.g. "Security Alert: 2.0.x") + // instead of per exact patch (e.g. "Security Alert: 2.0.1") + useMinorIssueTitle bool } var _ IssueWriter = &GithubIssueWriter{} @@ -48,6 +52,18 @@ func NewGithubIssueWriter(repo GithubRepo, client *github.Client, issuePredicate client: client, createGithubIssuePredicate: issuePredicate, allGithubIssues: nil, // initially nil, we'll lazy load these + useMinorIssueTitle: false, + } +} + +// NewGithubIssueWriterWithMinorTitle constructs a GithubIssueWriter that titles issues per minor version (X.Y.x) +func NewGithubIssueWriterWithMinorTitle(repo GithubRepo, client *github.Client, issuePredicate githubutils.RepositoryReleasePredicate) IssueWriter { + return &GithubIssueWriter{ + repo: repo, + client: client, + createGithubIssuePredicate: issuePredicate, + allGithubIssues: nil, + useMinorIssueTitle: true, } } @@ -92,7 +108,12 @@ func (g *GithubIssueWriter) Write( // will not be included in the filtered list versionToScan, _ := semver.NewVersion(release.GetTagName()) - issueTitle := fmt.Sprintf("Security Alert: %s", versionToScan.String()) + var issueTitle string + if g.useMinorIssueTitle { + issueTitle = fmt.Sprintf("Security Alert: %d.%d.x", versionToScan.Major(), versionToScan.Minor()) + } else { + issueTitle = fmt.Sprintf("Security Alert: %s", versionToScan.String()) + } issueRequest := &github.IssueRequest{ Title: github.String(issueTitle), Body: github.String(vulnerabilityMarkdown), diff --git a/securityscanutils/securityscan.go b/securityscanutils/securityscan.go index de67bd9b..d1e92a9c 100644 --- a/securityscanutils/securityscan.go +++ b/securityscanutils/securityscan.go @@ -104,6 +104,11 @@ type SecurityScanOpts struct { // Enable scanning of pre-release versions EnablePreRelease bool + + // Create/Update a single GitHub issue per minor version (e.g., "Security Alert: 2.0.x") + // rather than per exact patch version. When enabled, the writer will target a minor-scoped + // issue title. Scanning behavior is unchanged; it will scan the selected release(s). + CreateGithubIssueForMinorLatestPatchVersion bool } // GenerateSecurityScans generates .md files and writes them to the configured OutputDir for each repo @@ -193,7 +198,11 @@ func (s *SecurityScanner) initializeRepoConfiguration(ctx context.Context, repo issuePredicate = NewLatestPatchRepositoryReleasePredicate(releasesToScan) } if useGithubWriter { - repo.issueWriter = issuewriter.NewGithubIssueWriter(githubRepo, s.githubClient, issuePredicate) + if repo.Opts.CreateGithubIssueForMinorLatestPatchVersion { + repo.issueWriter = issuewriter.NewGithubIssueWriterWithMinorTitle(githubRepo, s.githubClient, issuePredicate) + } else { + repo.issueWriter = issuewriter.NewGithubIssueWriter(githubRepo, s.githubClient, issuePredicate) + } logger.Debugf("GithubIssueWriter configured with Predicate: %+v", issuePredicate) } else if repo.Opts.OutputResultLocally { repo.issueWriter, err = issuewriter.NewLocalIssueWriter(path.Join(repo.Opts.OutputDir, githubRepo.RepoName, "issue_results")) From 69e31febbb1e5b105e2a20d10f2eb8f7003adc47 Mon Sep 17 00:00:00 2001 From: rpunia Date: Wed, 14 Jan 2026 23:02:45 -0800 Subject: [PATCH 2/5] support issue-title-suffix --- securityscanutils/commands/scan_repo.go | 5 +++++ securityscanutils/issuewriter/github_writer.go | 12 ++++++++++-- securityscanutils/issuewriter/local_writer.go | 2 +- securityscanutils/securityscan.go | 7 +++++-- 4 files changed, 21 insertions(+), 5 deletions(-) diff --git a/securityscanutils/commands/scan_repo.go b/securityscanutils/commands/scan_repo.go index 9e48f5f0..1302d7e9 100644 --- a/securityscanutils/commands/scan_repo.go +++ b/securityscanutils/commands/scan_repo.go @@ -52,6 +52,8 @@ type scanRepoOptions struct { additionalContextFile string enablePreRelease bool + + issueTitleSuffix string } func (m *scanRepoOptions) addToFlags(flags *pflag.FlagSet) { @@ -66,6 +68,8 @@ func (m *scanRepoOptions) addToFlags(flags *pflag.FlagSet) { flags.StringVarP(&m.imagesVersionConstraintFile, "image-constraint-file", "i", "", "name of file with mapping of version to images") flags.StringVarP(&m.additionalContextFile, "additional-context-file", "d", "", "name of file with any additional context to add to the top of the generated vulnerability report") + flags.StringVar(&m.issueTitleSuffix, "issue-title-suffix", "", "text to append to the GitHub issue title (appended in parentheses)") + cliutils.MustMarkFlagRequired(flags, "github-repo") cliutils.MustMarkFlagRequired(flags, "release-constraint") cliutils.MustMarkFlagRequired(flags, "image-constraint-file") @@ -101,6 +105,7 @@ func doScanRepo(ctx context.Context, opts *scanRepoOptions) error { CreateGithubIssueForMinorLatestPatchVersion: opts.vulnerabilityAction == "github-issue-minor", AdditionalContext: additionalContext, EnablePreRelease: opts.enablePreRelease, + IssueTitleSuffix: opts.issueTitleSuffix, }, }, }, diff --git a/securityscanutils/issuewriter/github_writer.go b/securityscanutils/issuewriter/github_writer.go index 7917ea1e..25a69641 100644 --- a/securityscanutils/issuewriter/github_writer.go +++ b/securityscanutils/issuewriter/github_writer.go @@ -42,28 +42,33 @@ type GithubIssueWriter struct { // If true, the issue title will be created per minor (e.g. "Security Alert: 2.0.x") // instead of per exact patch (e.g. "Security Alert: 2.0.1") useMinorIssueTitle bool + + // Optional suffix appended to the issue title in parentheses. + titleSuffix string } var _ IssueWriter = &GithubIssueWriter{} -func NewGithubIssueWriter(repo GithubRepo, client *github.Client, issuePredicate githubutils.RepositoryReleasePredicate) IssueWriter { +func NewGithubIssueWriter(repo GithubRepo, client *github.Client, issuePredicate githubutils.RepositoryReleasePredicate, titleSuffix string) IssueWriter { return &GithubIssueWriter{ repo: repo, client: client, createGithubIssuePredicate: issuePredicate, allGithubIssues: nil, // initially nil, we'll lazy load these useMinorIssueTitle: false, + titleSuffix: titleSuffix, } } // NewGithubIssueWriterWithMinorTitle constructs a GithubIssueWriter that titles issues per minor version (X.Y.x) -func NewGithubIssueWriterWithMinorTitle(repo GithubRepo, client *github.Client, issuePredicate githubutils.RepositoryReleasePredicate) IssueWriter { +func NewGithubIssueWriterWithMinorTitle(repo GithubRepo, client *github.Client, issuePredicate githubutils.RepositoryReleasePredicate, titleSuffix string) IssueWriter { return &GithubIssueWriter{ repo: repo, client: client, createGithubIssuePredicate: issuePredicate, allGithubIssues: nil, useMinorIssueTitle: true, + titleSuffix: titleSuffix, } } @@ -114,6 +119,9 @@ func (g *GithubIssueWriter) Write( } else { issueTitle = fmt.Sprintf("Security Alert: %s", versionToScan.String()) } + if g.titleSuffix != "" { + issueTitle = fmt.Sprintf("%s (%s)", issueTitle, g.titleSuffix) + } issueRequest := &github.IssueRequest{ Title: github.String(issueTitle), Body: github.String(vulnerabilityMarkdown), diff --git a/securityscanutils/issuewriter/local_writer.go b/securityscanutils/issuewriter/local_writer.go index 32a29a73..97861b31 100644 --- a/securityscanutils/issuewriter/local_writer.go +++ b/securityscanutils/issuewriter/local_writer.go @@ -39,7 +39,7 @@ func (l *LocalIssueWriter) Write(_ context.Context, release *github.RepositoryRe if err != nil { return err } - _, err = fmt.Fprintf(f, contents) + _, err = fmt.Fprint(f, contents) if err != nil { return err } diff --git a/securityscanutils/securityscan.go b/securityscanutils/securityscan.go index d1e92a9c..a915c3ca 100644 --- a/securityscanutils/securityscan.go +++ b/securityscanutils/securityscan.go @@ -109,6 +109,9 @@ type SecurityScanOpts struct { // rather than per exact patch version. When enabled, the writer will target a minor-scoped // issue title. Scanning behavior is unchanged; it will scan the selected release(s). CreateGithubIssueForMinorLatestPatchVersion bool + + // Optional string appended to the GitHub issue title in parentheses. + IssueTitleSuffix string } // GenerateSecurityScans generates .md files and writes them to the configured OutputDir for each repo @@ -199,9 +202,9 @@ func (s *SecurityScanner) initializeRepoConfiguration(ctx context.Context, repo } if useGithubWriter { if repo.Opts.CreateGithubIssueForMinorLatestPatchVersion { - repo.issueWriter = issuewriter.NewGithubIssueWriterWithMinorTitle(githubRepo, s.githubClient, issuePredicate) + repo.issueWriter = issuewriter.NewGithubIssueWriterWithMinorTitle(githubRepo, s.githubClient, issuePredicate, repo.Opts.IssueTitleSuffix) } else { - repo.issueWriter = issuewriter.NewGithubIssueWriter(githubRepo, s.githubClient, issuePredicate) + repo.issueWriter = issuewriter.NewGithubIssueWriter(githubRepo, s.githubClient, issuePredicate, repo.Opts.IssueTitleSuffix) } logger.Debugf("GithubIssueWriter configured with Predicate: %+v", issuePredicate) } else if repo.Opts.OutputResultLocally { From 25235aa991c23f33422d95fa74ac5a7d299a47a9 Mon Sep 17 00:00:00 2001 From: rpunia Date: Mon, 16 Feb 2026 12:42:48 -0800 Subject: [PATCH 3/5] create issue for latest minor --- securityscanutils/securityscan.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/securityscanutils/securityscan.go b/securityscanutils/securityscan.go index a915c3ca..47d050ae 100644 --- a/securityscanutils/securityscan.go +++ b/securityscanutils/securityscan.go @@ -190,14 +190,16 @@ func (s *SecurityScanner) initializeRepoConfiguration(ctx context.Context, repo } // Default to not creating any issues var issuePredicate githubutils.RepositoryReleasePredicate = &githubutils.NoReleasesPredicate{} - useGithubWriter := repoOptions.CreateGithubIssuePerVersion || repoOptions.CreateGithubIssueForLatestPatchVersion + useGithubWriter := repoOptions.CreateGithubIssuePerVersion || + repoOptions.CreateGithubIssueForLatestPatchVersion || + repoOptions.CreateGithubIssueForMinorLatestPatchVersion if repoOptions.CreateGithubIssuePerVersion { // Create Github issue for all releases, if configured issuePredicate = &githubutils.AllReleasesPredicate{} } - if repoOptions.CreateGithubIssueForLatestPatchVersion { - // Create Github issues for all releases in the set + if repoOptions.CreateGithubIssueForLatestPatchVersion || repoOptions.CreateGithubIssueForMinorLatestPatchVersion { + // For both "latest" and "minor-latest" modes, only write for the latest patch releases issuePredicate = NewLatestPatchRepositoryReleasePredicate(releasesToScan) } if useGithubWriter { From 927d8b483697b2582ded66776928ea3e205615aa Mon Sep 17 00:00:00 2001 From: rpunia Date: Tue, 17 Feb 2026 08:21:58 -0800 Subject: [PATCH 4/5] get latest & add changelog --- changelog/v0.30.0/ci-update-gh-issue-writer.yaml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 changelog/v0.30.0/ci-update-gh-issue-writer.yaml diff --git a/changelog/v0.30.0/ci-update-gh-issue-writer.yaml b/changelog/v0.30.0/ci-update-gh-issue-writer.yaml new file mode 100644 index 00000000..fb7b6624 --- /dev/null +++ b/changelog/v0.30.0/ci-update-gh-issue-writer.yaml @@ -0,0 +1,6 @@ +changelog: + - type: NON_USER_FACING + description: | + Updated security scan GH issue writer to: + - create single issue for a given major.minor.patch, e.g. 2.1.x. + - append suffix to GH issue title so we can create multiple issues for a repo. \ No newline at end of file From 01e84b5b64315d01eb36e6333ad231146a938aab Mon Sep 17 00:00:00 2001 From: rpunia Date: Tue, 17 Feb 2026 08:23:17 -0800 Subject: [PATCH 5/5] mv changelog --- changelog/{v0.30.0 => v0.29.1}/ci-update-gh-issue-writer.yaml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename changelog/{v0.30.0 => v0.29.1}/ci-update-gh-issue-writer.yaml (100%) diff --git a/changelog/v0.30.0/ci-update-gh-issue-writer.yaml b/changelog/v0.29.1/ci-update-gh-issue-writer.yaml similarity index 100% rename from changelog/v0.30.0/ci-update-gh-issue-writer.yaml rename to changelog/v0.29.1/ci-update-gh-issue-writer.yaml