diff --git a/changelog/v0.29.1/ci-update-gh-issue-writer.yaml b/changelog/v0.29.1/ci-update-gh-issue-writer.yaml new file mode 100644 index 00000000..fb7b6624 --- /dev/null +++ b/changelog/v0.29.1/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 diff --git a/securityscanutils/commands/scan_repo.go b/securityscanutils/commands/scan_repo.go index 42aafb11..1302d7e9 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 @@ -51,13 +52,15 @@ type scanRepoOptions struct { additionalContextFile string enablePreRelease bool + + issueTitleSuffix string } 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") @@ -65,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") @@ -97,8 +102,10 @@ 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, + IssueTitleSuffix: opts.issueTitleSuffix, }, }, }, diff --git a/securityscanutils/issuewriter/github_writer.go b/securityscanutils/issuewriter/github_writer.go index dfc20911..25a69641 100644 --- a/securityscanutils/issuewriter/github_writer.go +++ b/securityscanutils/issuewriter/github_writer.go @@ -38,16 +38,37 @@ 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 + + // 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, titleSuffix string) IssueWriter { + return &GithubIssueWriter{ + repo: repo, + client: client, + createGithubIssuePredicate: issuePredicate, + allGithubIssues: nil, + useMinorIssueTitle: true, + titleSuffix: titleSuffix, } } @@ -92,7 +113,15 @@ 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()) + } + 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 de67bd9b..47d050ae 100644 --- a/securityscanutils/securityscan.go +++ b/securityscanutils/securityscan.go @@ -104,6 +104,14 @@ 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 + + // 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 @@ -182,18 +190,24 @@ 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 { - repo.issueWriter = issuewriter.NewGithubIssueWriter(githubRepo, s.githubClient, issuePredicate) + if repo.Opts.CreateGithubIssueForMinorLatestPatchVersion { + repo.issueWriter = issuewriter.NewGithubIssueWriterWithMinorTitle(githubRepo, s.githubClient, issuePredicate, repo.Opts.IssueTitleSuffix) + } else { + repo.issueWriter = issuewriter.NewGithubIssueWriter(githubRepo, s.githubClient, issuePredicate, repo.Opts.IssueTitleSuffix) + } 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"))