From a3ef46c30d3716dc77db6cbc6b72eb52fb46cf59 Mon Sep 17 00:00:00 2001 From: Christian Volk Date: Thu, 9 Jan 2025 16:57:03 +0100 Subject: [PATCH] feat: githubSecretScanningReport --- cmd/githubSecretScanningReport.go | 162 ++++++++++ cmd/githubSecretScanningReport_generated.go | 295 ++++++++++++++++++ ...thubSecretScanningReport_generated_test.go | 20 ++ cmd/githubSecretScanningReport_test.go | 146 +++++++++ cmd/metadata_generated.go | 1 + cmd/piper.go | 1 + .../metadata/githubSecretScanningReport.yaml | 86 +++++ 7 files changed, 711 insertions(+) create mode 100644 cmd/githubSecretScanningReport.go create mode 100644 cmd/githubSecretScanningReport_generated.go create mode 100644 cmd/githubSecretScanningReport_generated_test.go create mode 100644 cmd/githubSecretScanningReport_test.go create mode 100644 resources/metadata/githubSecretScanningReport.yaml diff --git a/cmd/githubSecretScanningReport.go b/cmd/githubSecretScanningReport.go new file mode 100644 index 0000000000..3de6c4794e --- /dev/null +++ b/cmd/githubSecretScanningReport.go @@ -0,0 +1,162 @@ +package cmd + +import ( + "context" + "encoding/json" + "fmt" + "io" + + "github.com/google/go-github/v45/github" + + "github.com/SAP/jenkins-library/pkg/command" + piperGithub "github.com/SAP/jenkins-library/pkg/github" + "github.com/SAP/jenkins-library/pkg/log" + "github.com/SAP/jenkins-library/pkg/piperutils" + "github.com/SAP/jenkins-library/pkg/telemetry" +) + +type githubClientWrapper struct { + client *github.Client +} + +func (gcw *githubClientWrapper) GetRepo(ctx context.Context, owner, repo string) (*github.Repository, *github.Response, error) { + return gcw.client.Repositories.Get(ctx, owner, repo) +} + +func (gcw *githubClientWrapper) ListAlertsForRepo(ctx context.Context, owner, repo string, opts *github.SecretScanningAlertListOptions) ([]*github.SecretScanningAlert, *github.Response, error) { + return gcw.client.SecretScanning.ListAlertsForRepo(ctx, owner, repo, opts) +} + +type githubSecretScanningReportUtils interface { + command.ExecRunner + + FileExists(filename string) (bool, error) + Create(name string) (io.ReadWriteCloser, error) + + GetRepo(ctx context.Context, owner, repo string) (*github.Repository, *github.Response, error) + ListAlertsForRepo(ctx context.Context, owner, repo string, opts *github.SecretScanningAlertListOptions) ([]*github.SecretScanningAlert, *github.Response, error) +} + +type githubSecretScanningReportUtilsBundle struct { + *command.Command + *piperutils.Files + *githubClientWrapper +} + +func newGithubSecretScanningReportUtils(ghClient *github.Client) githubSecretScanningReportUtils { + utils := githubSecretScanningReportUtilsBundle{ + Command: &command.Command{}, + Files: &piperutils.Files{}, + githubClientWrapper: &githubClientWrapper{ghClient}, + } + // Reroute command output to logging framework + utils.Stdout(log.Writer()) + utils.Stderr(log.Writer()) + return &utils +} + +func githubSecretScanningReport(config githubSecretScanningReportOptions, telemetryData *telemetry.CustomData) { + ctx, ghClient, err := piperGithub. + NewClientBuilder(config.Token, config.APIURL). + Build() + + if err != nil { + log.Entry().WithError(err).Fatal("Failed to get GitHub client.") + } + + // Utils can be used wherever the command.ExecRunner interface is expected. + // It can also be used for example as a mavenExecRunner. + utils := newGithubSecretScanningReportUtils(ghClient) + + // For HTTP calls import piperhttp "github.com/SAP/jenkins-library/pkg/http" + // and use a &piperhttp.Client{} in a custom system + // Example: step checkmarxExecuteScan.go + + // Error situations should be bubbled up until they reach the line below which will then stop execution + // through the log.Entry().Fatal() call leading to an os.Exit(1) in the end. + if err = runGithubSecretScanningReport(ctx, &config, telemetryData, utils); err != nil { + log.Entry().WithError(err).Fatal("step execution failed") + } +} + +func runGithubSecretScanningReport(ctx context.Context, config *githubSecretScanningReportOptions, telemetryData *telemetry.CustomData, utils githubSecretScanningReportUtils) error { + log.Entry().WithField("LogField", "Log field content").Info("This is just a demo for a simple step.") + + report, err := generateGithubSecretScanningReport(ctx, config, utils) + + if err != nil { + return fmt.Errorf("couldn't generate the github secret scanning report: %w", err) + } + + reportFile, err := utils.Create("github-secretscanning.report.json") + if err != nil { + return fmt.Errorf("couldn't create 'secretscan.json': %w", err) + } + + defer reportFile.Close() + + if err = json.NewEncoder(reportFile).Encode(report); err != nil { + return fmt.Errorf("couldn't save the github secret scanning report: %w", err) + } + + return nil +} + +// generateGithubSecretScanningReport generates a secret scanning report for a specified GitHub repository. +// It retrieves all open secret scanning alerts, compiles their details including secret type, state, +// and locations, and returns a structured report. If any error occurs during the retrieval process, +// the function returns an error. +func generateGithubSecretScanningReport(ctx context.Context, config *githubSecretScanningReportOptions, utils githubSecretScanningReportUtils) (*githubSecretScanningReportType, error) { + repo, _, err := utils.GetRepo(ctx, config.Owner, config.Repository) + + if err != nil { + return nil, err + } + + // query github for alerts + secretAlerts, _, err := utils.ListAlertsForRepo(ctx, config.Owner, config.Repository, &github.SecretScanningAlertListOptions{}) + if err != nil { + return nil, err + } + + alertsTotal := len(secretAlerts) + alertsAudited := 0 + + // query actual finding locations + for _, alert := range secretAlerts { + if alert.State != nil && *alert.State == "resolved" { + alertsAudited = alertsAudited + 1 + } + } + + report := &githubSecretScanningReportType{ + ToolName: "GitHubSecretScanning", + Findings: []githubSecretScanningFinding{ + githubSecretScanningFinding{ + ClassificationName: "Audit All", + Total: alertsTotal, + Audited: alertsAudited, + }, + }, + } + + if repo.HTMLURL != nil { + report.RepositoryURL = *repo.HTMLURL + report.SecretScanningURL = fmt.Sprintf("%s/security/secret-scanning", *repo.HTMLURL) + } + + return report, nil +} + +type githubSecretScanningReportType struct { + ToolName string `json:"toolName"` + RepositoryURL string `json:"repositoryUrl,omitempty"` + SecretScanningURL string `json:"secretScanningUrl,omitempty"` + Findings []githubSecretScanningFinding `json:"findings"` +} + +type githubSecretScanningFinding struct { + ClassificationName string `json:"classificationName"` + Total int `json:"total"` + Audited int `json:"audited"` +} diff --git a/cmd/githubSecretScanningReport_generated.go b/cmd/githubSecretScanningReport_generated.go new file mode 100644 index 0000000000..2862e0b4ed --- /dev/null +++ b/cmd/githubSecretScanningReport_generated.go @@ -0,0 +1,295 @@ +// Code generated by piper's step-generator. DO NOT EDIT. + +package cmd + +import ( + "fmt" + "os" + "reflect" + "strings" + "time" + + "github.com/SAP/jenkins-library/pkg/config" + "github.com/SAP/jenkins-library/pkg/gcp" + "github.com/SAP/jenkins-library/pkg/gcs" + "github.com/SAP/jenkins-library/pkg/log" + "github.com/SAP/jenkins-library/pkg/splunk" + "github.com/SAP/jenkins-library/pkg/telemetry" + "github.com/SAP/jenkins-library/pkg/validation" + "github.com/bmatcuk/doublestar" + "github.com/spf13/cobra" +) + +type githubSecretScanningReportOptions struct { + APIURL string `json:"apiUrl,omitempty"` + Owner string `json:"owner,omitempty"` + Repository string `json:"repository,omitempty"` + Token string `json:"token,omitempty"` +} + +type githubSecretScanningReportReports struct { +} + +func (p *githubSecretScanningReportReports) persist(stepConfig githubSecretScanningReportOptions, gcpJsonKeyFilePath string, gcsBucketId string, gcsFolderPath string, gcsSubFolder string) { + if gcsBucketId == "" { + log.Entry().Info("persisting reports to GCS is disabled, because gcsBucketId is empty") + return + } + log.Entry().Info("Uploading reports to Google Cloud Storage...") + content := []gcs.ReportOutputParam{ + {FilePattern: "**/secretscan.json", ParamRef: "", StepResultType: "secretscan"}, + } + envVars := []gcs.EnvVar{ + {Name: "GOOGLE_APPLICATION_CREDENTIALS", Value: gcpJsonKeyFilePath, Modified: false}, + } + gcsClient, err := gcs.NewClient(gcs.WithEnvVars(envVars)) + if err != nil { + log.Entry().Errorf("creation of GCS client failed: %v", err) + return + } + defer gcsClient.Close() + structVal := reflect.ValueOf(&stepConfig).Elem() + inputParameters := map[string]string{} + for i := 0; i < structVal.NumField(); i++ { + field := structVal.Type().Field(i) + if field.Type.String() == "string" { + paramName := strings.Split(field.Tag.Get("json"), ",") + paramValue, _ := structVal.Field(i).Interface().(string) + inputParameters[paramName[0]] = paramValue + } + } + if err := gcs.PersistReportsToGCS(gcsClient, content, inputParameters, gcsFolderPath, gcsBucketId, gcsSubFolder, doublestar.Glob, os.Stat); err != nil { + log.Entry().Errorf("failed to persist reports: %v", err) + } +} + +// GithubSecretScanningReportCommand Generates a report for GitHub secret scanning alerts for a specified GitHub repository. +func GithubSecretScanningReportCommand() *cobra.Command { + const STEP_NAME = "githubSecretScanningReport" + + metadata := githubSecretScanningReportMetadata() + var stepConfig githubSecretScanningReportOptions + var startTime time.Time + var reports githubSecretScanningReportReports + var logCollector *log.CollectorHook + var splunkClient *splunk.Splunk + telemetryClient := &telemetry.Telemetry{} + + var createGithubSecretScanningReportCmd = &cobra.Command{ + Use: STEP_NAME, + Short: "Generates a report for GitHub secret scanning alerts for a specified GitHub repository.", + Long: `This step retrieves the secret scan report from your specified GitHub repository. +It can be used to automate the fetching of reports for security audits, compliance checks, +or general monitoring of secrets and sensitive information detected by GitHub. + +The fetched report can include details such as: + +* Line numbers and file names where potential secrets were detected +* Types of secrets identified (e.g., credentials, tokens) +* Severity levels of each detected secret`, + PreRunE: func(cmd *cobra.Command, _ []string) error { + startTime = time.Now() + log.SetStepName(STEP_NAME) + log.SetVerbose(GeneralConfig.Verbose) + + GeneralConfig.GitHubAccessTokens = ResolveAccessTokens(GeneralConfig.GitHubTokens) + + path, err := os.Getwd() + if err != nil { + return err + } + fatalHook := &log.FatalHook{CorrelationID: GeneralConfig.CorrelationID, Path: path} + log.RegisterHook(fatalHook) + + err = PrepareConfig(cmd, &metadata, STEP_NAME, &stepConfig, config.OpenPiperFile) + if err != nil { + log.SetErrorCategory(log.ErrorConfiguration) + return err + } + log.RegisterSecret(stepConfig.Token) + + if len(GeneralConfig.HookConfig.SentryConfig.Dsn) > 0 { + sentryHook := log.NewSentryHook(GeneralConfig.HookConfig.SentryConfig.Dsn, GeneralConfig.CorrelationID) + log.RegisterHook(&sentryHook) + } + + if len(GeneralConfig.HookConfig.SplunkConfig.Dsn) > 0 || len(GeneralConfig.HookConfig.SplunkConfig.ProdCriblEndpoint) > 0 { + splunkClient = &splunk.Splunk{} + logCollector = &log.CollectorHook{CorrelationID: GeneralConfig.CorrelationID} + log.RegisterHook(logCollector) + } + + if err = log.RegisterANSHookIfConfigured(GeneralConfig.CorrelationID); err != nil { + log.Entry().WithError(err).Warn("failed to set up SAP Alert Notification Service log hook") + } + + validation, err := validation.New(validation.WithJSONNamesForStructFields(), validation.WithPredefinedErrorMessages()) + if err != nil { + return err + } + if err = validation.ValidateStruct(stepConfig); err != nil { + log.SetErrorCategory(log.ErrorConfiguration) + return err + } + + return nil + }, + Run: func(_ *cobra.Command, _ []string) { + vaultClient := config.GlobalVaultClient() + if vaultClient != nil { + defer vaultClient.MustRevokeToken() + } + + stepTelemetryData := telemetry.CustomData{} + stepTelemetryData.ErrorCode = "1" + handler := func() { + reports.persist(stepConfig, GeneralConfig.GCPJsonKeyFilePath, GeneralConfig.GCSBucketId, GeneralConfig.GCSFolderPath, GeneralConfig.GCSSubFolder) + config.RemoveVaultSecretFiles() + stepTelemetryData.Duration = fmt.Sprintf("%v", time.Since(startTime).Milliseconds()) + stepTelemetryData.ErrorCategory = log.GetErrorCategory().String() + stepTelemetryData.PiperCommitHash = GitCommit + telemetryClient.SetData(&stepTelemetryData) + telemetryClient.Send() + if len(GeneralConfig.HookConfig.SplunkConfig.Dsn) > 0 { + splunkClient.Initialize(GeneralConfig.CorrelationID, + GeneralConfig.HookConfig.SplunkConfig.Dsn, + GeneralConfig.HookConfig.SplunkConfig.Token, + GeneralConfig.HookConfig.SplunkConfig.Index, + GeneralConfig.HookConfig.SplunkConfig.SendLogs) + splunkClient.Send(telemetryClient.GetData(), logCollector) + } + if len(GeneralConfig.HookConfig.SplunkConfig.ProdCriblEndpoint) > 0 { + splunkClient.Initialize(GeneralConfig.CorrelationID, + GeneralConfig.HookConfig.SplunkConfig.ProdCriblEndpoint, + GeneralConfig.HookConfig.SplunkConfig.ProdCriblToken, + GeneralConfig.HookConfig.SplunkConfig.ProdCriblIndex, + GeneralConfig.HookConfig.SplunkConfig.SendLogs) + splunkClient.Send(telemetryClient.GetData(), logCollector) + } + if GeneralConfig.HookConfig.GCPPubSubConfig.Enabled { + err := gcp.NewGcpPubsubClient( + vaultClient, + GeneralConfig.HookConfig.GCPPubSubConfig.ProjectNumber, + GeneralConfig.HookConfig.GCPPubSubConfig.IdentityPool, + GeneralConfig.HookConfig.GCPPubSubConfig.IdentityProvider, + GeneralConfig.CorrelationID, + GeneralConfig.HookConfig.OIDCConfig.RoleID, + ).Publish(GeneralConfig.HookConfig.GCPPubSubConfig.Topic, telemetryClient.GetDataBytes()) + if err != nil { + log.Entry().WithError(err).Warn("event publish failed") + } + } + } + log.DeferExitHandler(handler) + defer handler() + telemetryClient.Initialize(GeneralConfig.NoTelemetry, STEP_NAME, GeneralConfig.HookConfig.PendoConfig.Token) + githubSecretScanningReport(stepConfig, &stepTelemetryData) + stepTelemetryData.ErrorCode = "0" + log.Entry().Info("SUCCESS") + }, + } + + addGithubSecretScanningReportFlags(createGithubSecretScanningReportCmd, &stepConfig) + return createGithubSecretScanningReportCmd +} + +func addGithubSecretScanningReportFlags(cmd *cobra.Command, stepConfig *githubSecretScanningReportOptions) { + cmd.Flags().StringVar(&stepConfig.APIURL, "apiUrl", `https://api.github.com`, "Set the GitHub API url.") + cmd.Flags().StringVar(&stepConfig.Owner, "owner", os.Getenv("PIPER_owner"), "Name of the GitHub organization.") + cmd.Flags().StringVar(&stepConfig.Repository, "repository", os.Getenv("PIPER_repository"), "Name of the GitHub repository.") + cmd.Flags().StringVar(&stepConfig.Token, "token", os.Getenv("PIPER_token"), "GitHub personal access token as per https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line") + + cmd.MarkFlagRequired("apiUrl") + cmd.MarkFlagRequired("owner") + cmd.MarkFlagRequired("repository") + cmd.MarkFlagRequired("token") +} + +// retrieve step metadata +func githubSecretScanningReportMetadata() config.StepData { + var theMetaData = config.StepData{ + Metadata: config.StepMetadata{ + Name: "githubSecretScanningReport", + Aliases: []config.Alias{}, + Description: "Generates a report for GitHub secret scanning alerts for a specified GitHub repository.", + }, + Spec: config.StepSpec{ + Inputs: config.StepInputs{ + Secrets: []config.StepSecrets{ + {Name: "githubTokenCredentialsId", Description: "Jenkins 'Secret text' credentials ID containing token to authenticate to GitHub.", Type: "jenkins"}, + }, + Parameters: []config.StepParameters{ + { + Name: "apiUrl", + ResourceRef: []config.ResourceReference{}, + Scope: []string{"GENERAL", "PARAMETERS", "STAGES", "STEPS"}, + Type: "string", + Mandatory: true, + Aliases: []config.Alias{{Name: "githubApiUrl"}}, + Default: `https://api.github.com`, + }, + { + Name: "owner", + ResourceRef: []config.ResourceReference{ + { + Name: "commonPipelineEnvironment", + Param: "github/owner", + }, + }, + Scope: []string{"GENERAL", "PARAMETERS", "STAGES", "STEPS"}, + Type: "string", + Mandatory: true, + Aliases: []config.Alias{{Name: "githubOrg"}}, + Default: os.Getenv("PIPER_owner"), + }, + { + Name: "repository", + ResourceRef: []config.ResourceReference{ + { + Name: "commonPipelineEnvironment", + Param: "github/repository", + }, + }, + Scope: []string{"GENERAL", "PARAMETERS", "STAGES", "STEPS"}, + Type: "string", + Mandatory: true, + Aliases: []config.Alias{{Name: "githubRepo"}}, + Default: os.Getenv("PIPER_repository"), + }, + { + Name: "token", + ResourceRef: []config.ResourceReference{ + { + Name: "githubTokenCredentialsId", + Type: "secret", + }, + + { + Name: "githubVaultSecretName", + Type: "vaultSecret", + Default: "github", + }, + }, + Scope: []string{"GENERAL", "PARAMETERS", "STAGES", "STEPS"}, + Type: "string", + Mandatory: true, + Aliases: []config.Alias{{Name: "githubToken"}, {Name: "access_token"}}, + Default: os.Getenv("PIPER_token"), + }, + }, + }, + Outputs: config.StepOutputs{ + Resources: []config.StepResources{ + { + Name: "reports", + Type: "reports", + Parameters: []map[string]interface{}{ + {"filePattern": "**/secretscan.json", "type": "secretscan"}, + }, + }, + }, + }, + }, + } + return theMetaData +} diff --git a/cmd/githubSecretScanningReport_generated_test.go b/cmd/githubSecretScanningReport_generated_test.go new file mode 100644 index 0000000000..e8fbd712e5 --- /dev/null +++ b/cmd/githubSecretScanningReport_generated_test.go @@ -0,0 +1,20 @@ +//go:build unit +// +build unit + +package cmd + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGithubSecretScanningReportCommand(t *testing.T) { + t.Parallel() + + testCmd := GithubSecretScanningReportCommand() + + // only high level testing performed - details are tested in step generation procedure + assert.Equal(t, "githubSecretScanningReport", testCmd.Use, "command name incorrect") + +} diff --git a/cmd/githubSecretScanningReport_test.go b/cmd/githubSecretScanningReport_test.go new file mode 100644 index 0000000000..a597668a68 --- /dev/null +++ b/cmd/githubSecretScanningReport_test.go @@ -0,0 +1,146 @@ +package cmd + +import ( + "context" + "encoding/json" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + + "github.com/google/go-github/v45/github" + + piperMock "github.com/SAP/jenkins-library/pkg/mock" +) + +var fakeGithubURL = "https://github.local/myorg/myrepository" + +type githubSecretScanningServiceMock struct { + mock.Mock +} + +func (gssm *githubSecretScanningServiceMock) GetRepo(ctx context.Context, owner, repo string) (*github.Repository, *github.Response, error) { + args := gssm.Called(ctx, owner, repo) + return args.Get(0).(*github.Repository), args.Get(1).(*github.Response), args.Error(2) +} + +func (gssm *githubSecretScanningServiceMock) ListAlertsForRepo(ctx context.Context, owner, repo string, opts *github.SecretScanningAlertListOptions) ([]*github.SecretScanningAlert, *github.Response, error) { + args := gssm.Called(ctx, owner, repo, opts) + return args.Get(0).([]*github.SecretScanningAlert), args.Get(1).(*github.Response), args.Error(2) +} + +type githubSecretScanningMockUtils struct { + *piperMock.ExecMockRunner + *piperMock.FilesMock + *githubSecretScanningServiceMock +} + +func newGithubSecretScanTestsUtils() githubSecretScanningMockUtils { + utils := githubSecretScanningMockUtils{ + ExecMockRunner: &piperMock.ExecMockRunner{}, + FilesMock: &piperMock.FilesMock{}, + githubSecretScanningServiceMock: &githubSecretScanningServiceMock{}, + } + return utils +} + +func TestRunGithubSecretScanningReport(t *testing.T) { + t.Parallel() + + t.Run("happy path - no findings in repository", func(t *testing.T) { + config := githubSecretScanningReportOptions{ + Owner: "myorg", + Repository: "myrepository", + } + + ctx := context.Background() + + utils := newGithubSecretScanTestsUtils() + + utils.On("GetRepo", ctx, config.Owner, config.Repository).Return(&github.Repository{HTMLURL: &fakeGithubURL}, &github.Response{}, nil) + utils.On("ListAlertsForRepo", ctx, config.Owner, config.Repository, mock.Anything).Return([]*github.SecretScanningAlert{}, &github.Response{}, nil) + + // test + err := runGithubSecretScanningReport(ctx, &config, nil, utils) + + // assert + assert.NoError(t, err) + + if reportRWC, err := utils.Open("github-secretscanning.report.json"); assert.NoError(t, err) { + defer reportRWC.Close() + + var report githubSecretScanningReportType + if err = json.NewDecoder(reportRWC).Decode(&report); assert.NoError(t, err) { + assert.Equal(t, "GitHubSecretScanning", report.ToolName) + assert.Equal(t, "https://github.local/myorg/myrepository", report.RepositoryURL) + assert.Equal(t, "https://github.local/myorg/myrepository/security/secret-scanning", report.SecretScanningURL) + + if assert.Len(t, report.Findings, 1) { + assert.Equal(t, "Audit All", report.Findings[0].ClassificationName) + assert.Equal(t, 0, report.Findings[0].Total) + assert.Equal(t, 0, report.Findings[0].Audited) + } + } + } + }) + + t.Run("happy path - repo has findings", func(t *testing.T) { + config := githubSecretScanningReportOptions{ + Owner: "myorg", + Repository: "myrepository", + } + + ctx := context.Background() + + utils := newGithubSecretScanTestsUtils() + + resolved := "resolved" + + utils.On("GetRepo", ctx, config.Owner, config.Repository).Return(&github.Repository{HTMLURL: &fakeGithubURL}, &github.Response{}, nil) + utils.On("ListAlertsForRepo", ctx, config.Owner, config.Repository, mock.Anything).Return([]*github.SecretScanningAlert{ + &github.SecretScanningAlert{State: &resolved}, + &github.SecretScanningAlert{}, + }, &github.Response{}, nil) + + // test + err := runGithubSecretScanningReport(ctx, &config, nil, utils) + + // assert + assert.NoError(t, err) + + if reportRWC, err := utils.Open("github-secretscanning.report.json"); assert.NoError(t, err) { + defer reportRWC.Close() + + var report githubSecretScanningReportType + if err = json.NewDecoder(reportRWC).Decode(&report); assert.NoError(t, err) { + assert.Equal(t, "GitHubSecretScanning", report.ToolName) + assert.Equal(t, "https://github.local/myorg/myrepository", report.RepositoryURL) + assert.Equal(t, "https://github.local/myorg/myrepository/security/secret-scanning", report.SecretScanningURL) + + if assert.Len(t, report.Findings, 1) { + assert.Equal(t, "Audit All", report.Findings[0].ClassificationName) + assert.Equal(t, 2, report.Findings[0].Total) + assert.Equal(t, 1, report.Findings[0].Audited) + } + } + } + }) + + t.Run("error path - simulate repo not found", func(t *testing.T) { + config := githubSecretScanningReportOptions{ + Owner: "myorg", + Repository: "myrepository", + } + ctx := context.Background() + utils := newGithubSecretScanTestsUtils() + + utils.On("GetRepo", ctx, config.Owner, config.Repository).Return(&github.Repository{}, &github.Response{}, fmt.Errorf("not found")) + + // test + err := runGithubSecretScanningReport(ctx, &config, nil, utils) + + // assert + assert.EqualError(t, err, "couldn't generate the github secret scanning report: not found") + }) +} diff --git a/cmd/metadata_generated.go b/cmd/metadata_generated.go index 50b0d6dadb..fd698f4071 100644 --- a/cmd/metadata_generated.go +++ b/cmd/metadata_generated.go @@ -70,6 +70,7 @@ func GetAllStepMetadata() map[string]config.StepData { "githubCreateIssue": githubCreateIssueMetadata(), "githubCreatePullRequest": githubCreatePullRequestMetadata(), "githubPublishRelease": githubPublishReleaseMetadata(), + "githubSecretScanningReport": githubSecretScanningReportMetadata(), "githubSetCommitStatus": githubSetCommitStatusMetadata(), "gitopsUpdateDeployment": gitopsUpdateDeploymentMetadata(), "golangBuild": golangBuildMetadata(), diff --git a/cmd/piper.go b/cmd/piper.go index ed728f8099..25735d2719 100644 --- a/cmd/piper.go +++ b/cmd/piper.go @@ -136,6 +136,7 @@ func Execute() { rootCmd.AddCommand(GithubCreatePullRequestCommand()) rootCmd.AddCommand(GithubPublishReleaseCommand()) rootCmd.AddCommand(GithubSetCommitStatusCommand()) + rootCmd.AddCommand(GithubSecretScanningReportCommand()) rootCmd.AddCommand(GitopsUpdateDeploymentCommand()) rootCmd.AddCommand(CloudFoundryDeleteServiceCommand()) rootCmd.AddCommand(AbapEnvironmentPullGitRepoCommand()) diff --git a/resources/metadata/githubSecretScanningReport.yaml b/resources/metadata/githubSecretScanningReport.yaml new file mode 100644 index 0000000000..0c64e482cd --- /dev/null +++ b/resources/metadata/githubSecretScanningReport.yaml @@ -0,0 +1,86 @@ +metadata: + name: githubSecretScanningReport + description: Generates a report for GitHub secret scanning alerts for a specified GitHub repository. + longDescription: | + This step retrieves the secret scan report from your specified GitHub repository. + It can be used to automate the fetching of reports for security audits, compliance checks, + or general monitoring of secrets and sensitive information detected by GitHub. + + The fetched report can include details such as: + + * Line numbers and file names where potential secrets were detected + * Types of secrets identified (e.g., credentials, tokens) + * Severity levels of each detected secret +spec: + inputs: + secrets: + - name: githubTokenCredentialsId + description: Jenkins 'Secret text' credentials ID containing token to authenticate to GitHub. + type: jenkins + params: + - name: apiUrl + aliases: + - name: githubApiUrl + description: Set the GitHub API url. + scope: + - GENERAL + - PARAMETERS + - STAGES + - STEPS + type: string + default: https://api.github.com + mandatory: true + - name: owner + aliases: + - name: githubOrg + description: Name of the GitHub organization. + resourceRef: + - name: commonPipelineEnvironment + param: github/owner + scope: + - GENERAL + - PARAMETERS + - STAGES + - STEPS + type: string + mandatory: true + - name: repository + aliases: + - name: githubRepo + description: Name of the GitHub repository. + resourceRef: + - name: commonPipelineEnvironment + param: github/repository + scope: + - GENERAL + - PARAMETERS + - STAGES + - STEPS + type: string + mandatory: true + - name: token + aliases: + - name: githubToken + - name: access_token + description: "GitHub personal access token as per https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line" + scope: + - GENERAL + - PARAMETERS + - STAGES + - STEPS + type: string + mandatory: true + secret: true + resourceRef: + - name: githubTokenCredentialsId + type: secret + - type: vaultSecret + default: github + name: githubVaultSecretName + outputs: + resources: + - name: reports + type: reports + params: + - filePattern: "**/github-secretscanning.report.json" + type: secretscanning