diff --git a/ci-runner/env_gen.json b/ci-runner/env_gen.json index 5334f14fd..4ad2a1d33 100644 --- a/ci-runner/env_gen.json +++ b/ci-runner/env_gen.json @@ -1 +1 @@ -[{"Category":"DEVTRON","Fields":[{"Env":"AZURE_ACCOUNT_KEY","EnvType":"string","EnvValue":"","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"AZURE_ACCOUNT_NAME","EnvType":"string","EnvValue":"","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"AZURE_BLOB_CONTAINER_CI_CACHE","EnvType":"string","EnvValue":"","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"AZURE_BLOB_CONTAINER_CI_LOG","EnvType":"string","EnvValue":"","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"AZURE_GATEWAY_CONNECTION_INSECURE","EnvType":"bool","EnvValue":"true","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"AZURE_GATEWAY_URL","EnvType":"string","EnvValue":"","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"BLOB_STORAGE_GCP_CREDENTIALS_JSON","EnvType":"string","EnvValue":"","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"BLOB_STORAGE_PROVIDER","EnvType":"","EnvValue":"","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"BLOB_STORAGE_S3_ACCESS_KEY","EnvType":"string","EnvValue":"","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"BLOB_STORAGE_S3_BUCKET_VERSIONED","EnvType":"bool","EnvValue":"true","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"BLOB_STORAGE_S3_ENDPOINT","EnvType":"string","EnvValue":"","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"BLOB_STORAGE_S3_ENDPOINT_INSECURE","EnvType":"bool","EnvValue":"false","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"BLOB_STORAGE_S3_SECRET_KEY","EnvType":"string","EnvValue":"","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"CONSUMER_CONFIG_JSON","EnvType":"string","EnvValue":"","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"DEFAULT_BUILD_LOGS_BUCKET","EnvType":"string","EnvValue":"","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"DEFAULT_CACHE_BUCKET","EnvType":"string","EnvValue":"","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"DEFAULT_CACHE_BUCKET_REGION","EnvType":"string","EnvValue":"","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"DEFAULT_CD_LOGS_BUCKET_REGION","EnvType":"string","EnvValue":"","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"DEFAULT_LOG_TIME_LIMIT","EnvType":"int64","EnvValue":"1","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"IMAGE_SCANNER_ENDPOINT","EnvType":"string","EnvValue":"http://image-scanner-new-demo-devtroncd-service.devtroncd:80","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"LOG_LEVEL","EnvType":"int","EnvValue":"0","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"NATS_MSG_ACK_WAIT_IN_SECS","EnvType":"int","EnvValue":"120","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"NATS_MSG_BUFFER_SIZE","EnvType":"int","EnvValue":"-1","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"NATS_MSG_MAX_AGE","EnvType":"int","EnvValue":"86400","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"NATS_MSG_PROCESSING_BATCH_SIZE","EnvType":"int","EnvValue":"1","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"NATS_MSG_REPLICAS","EnvType":"int","EnvValue":"0","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"NATS_SERVER_HOST","EnvType":"string","EnvValue":"nats://devtron-nats.devtroncd:4222","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"PG_EXPORT_PROM_METRICS","EnvType":"bool","EnvValue":"true","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"PG_LOG_ALL_FAILURE_QUERIES","EnvType":"bool","EnvValue":"true","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"PG_LOG_ALL_QUERY","EnvType":"bool","EnvValue":"false","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"PG_LOG_SLOW_QUERY","EnvType":"bool","EnvValue":"true","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"PG_QUERY_DUR_THRESHOLD","EnvType":"int64","EnvValue":"5000","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"SHOW_DOCKER_BUILD_ARGS","EnvType":"bool","EnvValue":"true","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"STREAM_CONFIG_JSON","EnvType":"string","EnvValue":"","EnvDescription":"","Example":"","Deprecated":"false"}]}] \ No newline at end of file +[{"Category":"DEVTRON","Fields":[{"Env":"AZURE_ACCOUNT_KEY","EnvType":"string","EnvValue":"","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"AZURE_ACCOUNT_NAME","EnvType":"string","EnvValue":"","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"AZURE_BLOB_CONTAINER_CI_CACHE","EnvType":"string","EnvValue":"","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"AZURE_BLOB_CONTAINER_CI_LOG","EnvType":"string","EnvValue":"","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"AZURE_GATEWAY_CONNECTION_INSECURE","EnvType":"bool","EnvValue":"true","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"AZURE_GATEWAY_URL","EnvType":"string","EnvValue":"","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"BLOB_STORAGE_GCP_CREDENTIALS_JSON","EnvType":"string","EnvValue":"","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"BLOB_STORAGE_PROVIDER","EnvType":"","EnvValue":"","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"BLOB_STORAGE_S3_ACCESS_KEY","EnvType":"string","EnvValue":"","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"BLOB_STORAGE_S3_BUCKET_VERSIONED","EnvType":"bool","EnvValue":"true","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"BLOB_STORAGE_S3_ENDPOINT","EnvType":"string","EnvValue":"","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"BLOB_STORAGE_S3_ENDPOINT_INSECURE","EnvType":"bool","EnvValue":"false","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"BLOB_STORAGE_S3_SECRET_KEY","EnvType":"string","EnvValue":"","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"CONSUMER_CONFIG_JSON","EnvType":"string","EnvValue":"","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"DEFAULT_BUILD_LOGS_BUCKET","EnvType":"string","EnvValue":"","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"DEFAULT_CACHE_BUCKET","EnvType":"string","EnvValue":"","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"DEFAULT_CACHE_BUCKET_REGION","EnvType":"string","EnvValue":"","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"DEFAULT_CD_LOGS_BUCKET_REGION","EnvType":"string","EnvValue":"","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"DEFAULT_LOG_TIME_LIMIT","EnvType":"int64","EnvValue":"1","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"DOCKERFILE_SCAN_FAIL_ON_ERROR","EnvType":"bool","EnvValue":"false","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"DOCKERFILE_SCAN_MAX_RETRIES","EnvType":"int","EnvValue":"3","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"DOCKERFILE_SCAN_RETRY_WAIT_SECONDS","EnvType":"int","EnvValue":"5","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"IMAGE_SCANNER_ENDPOINT","EnvType":"string","EnvValue":"http://image-scanner-service.devtroncd:80","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"LOG_LEVEL","EnvType":"int","EnvValue":"0","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"NATS_MSG_ACK_WAIT_IN_SECS","EnvType":"int","EnvValue":"120","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"NATS_MSG_BUFFER_SIZE","EnvType":"int","EnvValue":"-1","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"NATS_MSG_MAX_AGE","EnvType":"int","EnvValue":"86400","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"NATS_MSG_PROCESSING_BATCH_SIZE","EnvType":"int","EnvValue":"1","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"NATS_MSG_REPLICAS","EnvType":"int","EnvValue":"0","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"NATS_SERVER_HOST","EnvType":"string","EnvValue":"nats://devtron-nats.devtroncd:4222","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"PG_EXPORT_PROM_METRICS","EnvType":"bool","EnvValue":"true","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"PG_LOG_ALL_FAILURE_QUERIES","EnvType":"bool","EnvValue":"true","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"PG_LOG_ALL_QUERY","EnvType":"bool","EnvValue":"false","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"PG_LOG_SLOW_QUERY","EnvType":"bool","EnvValue":"true","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"PG_QUERY_DUR_THRESHOLD","EnvType":"int64","EnvValue":"5000","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"SHOW_DOCKER_BUILD_ARGS","EnvType":"bool","EnvValue":"true","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"STREAM_CONFIG_JSON","EnvType":"string","EnvValue":"","EnvDescription":"","Example":"","Deprecated":"false"}]}] \ No newline at end of file diff --git a/ci-runner/env_gen.md b/ci-runner/env_gen.md index 033d7a119..7705664d7 100644 --- a/ci-runner/env_gen.md +++ b/ci-runner/env_gen.md @@ -22,7 +22,10 @@ | DEFAULT_CACHE_BUCKET_REGION | string | | | | false | | DEFAULT_CD_LOGS_BUCKET_REGION | string | | | | false | | DEFAULT_LOG_TIME_LIMIT | int64 |1 | | | false | - | IMAGE_SCANNER_ENDPOINT | string |http://image-scanner-new-demo-devtroncd-service.devtroncd:80 | | | false | + | DOCKERFILE_SCAN_FAIL_ON_ERROR | bool |false | | | false | + | DOCKERFILE_SCAN_MAX_RETRIES | int |3 | | | false | + | DOCKERFILE_SCAN_RETRY_WAIT_SECONDS | int |5 | | | false | + | IMAGE_SCANNER_ENDPOINT | string |http://image-scanner-service.devtroncd:80 | | | false | | LOG_LEVEL | int |0 | | | false | | NATS_MSG_ACK_WAIT_IN_SECS | int |120 | | | false | | NATS_MSG_BUFFER_SIZE | int |-1 | | | false | diff --git a/ci-runner/executor/stage/ciStages.go b/ci-runner/executor/stage/ciStages.go index 220cdc397..42da4ffae 100644 --- a/ci-runner/executor/stage/ciStages.go +++ b/ci-runner/executor/stage/ciStages.go @@ -348,6 +348,28 @@ func (impl *CiStage) runBuildArtifact(ciCdRequest *helper.CiCdTriggerEvent, metr // build start := time.Now() metrics.BuildStartTime = start + + // Trigger Dockerfile scan right before build (git clone has definitely completed) + // Check BOTH flags: user's choice OR policy override + if ciCdRequest.CommonWorkflowRequest.DockerfileScanEnabled || ciCdRequest.CommonWorkflowRequest.ForceDockerfileScan { + log.Println(util.DEVTRON, "dockerfile scan triggered at build start (git clone completed)", + "buildId", ciCdRequest.CommonWorkflowRequest.WorkflowId, + "pipelineId", ciCdRequest.CommonWorkflowRequest.PipelineId, + "appId", ciCdRequest.CommonWorkflowRequest.AppId, + "checkoutPath", ciCdRequest.CommonWorkflowRequest.CheckoutPath) + // Trigger scan asynchronously (non-blocking, runs parallel to build) + go func() { + log.Println(util.DEVTRON, "dockerfile scan started", + "appId", ciCdRequest.CommonWorkflowRequest.AppId, + "buildId", ciCdRequest.CommonWorkflowRequest.WorkflowId, + "pipelineId", ciCdRequest.CommonWorkflowRequest.PipelineId, + "checkoutPath", ciCdRequest.CommonWorkflowRequest.CheckoutPath) + helper.InitiateDockerfileScan(ciCdRequest.CommonWorkflowRequest) + log.Println(util.DEVTRON, "dockerfile scan request sent to image-scanner", + "buildId", ciCdRequest.CommonWorkflowRequest.WorkflowId) + }() + } + dest, err := impl.dockerHelper.BuildArtifact(ciCdRequest.CommonWorkflowRequest) // TODO make it skipable metrics.BuildDuration = time.Since(start).Seconds() if err != nil { diff --git a/ci-runner/helper/DockerfileScanHelper.go b/ci-runner/helper/DockerfileScanHelper.go new file mode 100644 index 000000000..1adbda79b --- /dev/null +++ b/ci-runner/helper/DockerfileScanHelper.go @@ -0,0 +1,180 @@ +package helper + +import ( + "crypto/tls" + "encoding/json" + "fmt" + "log" + "net/http" + "os" + "path/filepath" + "time" + + "github.com/caarlos0/env" + "github.com/devtron-labs/ci-runner/util" + "github.com/go-resty/resty/v2" +) + +// DockerfileScanRequest represents the request to scan a Dockerfile +type DockerfileScanRequest struct { + AppId int `json:"appId"` + BuildId int `json:"buildId"` + PipelineId int `json:"pipelineId"` + DockerfileContent string `json:"dockerfileContent"` + DockerfileScanEnabled bool `json:"dockerfileScanEnabled"` + ForceDockerfileScan bool `json:"forceDockerfileScan"` + IgnoredRules []string `json:"ignoredRules"` +} + +// ScanConfig holds configuration for Dockerfile scanning +type ScanConfig struct { + ImageScannerEndpoint string `env:"IMAGE_SCANNER_ENDPOINT" envDefault:"http://image-scanner-service.devtroncd:80"` + FailOnError bool `env:"DOCKERFILE_SCAN_FAIL_ON_ERROR" envDefault:"false"` + MaxRetries int `env:"DOCKERFILE_SCAN_MAX_RETRIES" envDefault:"3"` + RetryWaitTimeSeconds int `env:"DOCKERFILE_SCAN_RETRY_WAIT_SECONDS" envDefault:"5"` +} + +// MaxDockerfileSize is the maximum allowed Dockerfile size (1MB) +const MaxDockerfileSize = 1 * 1024 * 1024 // 1MB + +// InitiateDockerfileScan initiates a Dockerfile scan using hadolint +// It reads the Dockerfile from filesystem and sends content to image-scanner service +// Note: The decision to run the scan is made by the caller (runBuildArtifact) +// which handles FORCE_DOCKERFILE_SCAN flag and pipeline-level DockerfileScanEnabled settings +func InitiateDockerfileScan(ciRequest *CommonWorkflowRequest) { + log.Println(util.DEVTRON, "initiating Dockerfile scan") + + // Validate config exists + if ciRequest.CiBuildConfig == nil || ciRequest.CiBuildConfig.DockerBuildConfig == nil { + log.Println(util.DEVTRON, "docker build config not found, skipping Dockerfile scan") + return + } + + // Wait for git clone to complete (checkout path to exist) + // Use the SAME path resolution as Docker build (getDockerfilePath) + var dockerfilePath string + if ciRequest.CiBuildConfig.CiBuildType == "managed-dockerfile-build" { + // For managed Dockerfile, use GetSelfManagedDockerfilePath + dockerfilePath = filepath.Join(util.WORKINGDIR, ciRequest.CheckoutPath, "./Dockerfile") + } else { + // For self-managed Dockerfile, use the configured path + dockerfilePath = ciRequest.CiBuildConfig.DockerBuildConfig.DockerfilePath + } + // Convert to absolute path (same as Docker build) + dockerfilePath, _ = filepath.Abs(dockerfilePath) + + // Fallback wait (should not be needed - scan triggered at Docker build start) + maxWait := 2 * time.Minute + waitInterval := 10 * time.Second + startTime := time.Now() + + log.Println(util.DEVTRON, "dockerfile scan: waiting for git clone to complete", "path", dockerfilePath, "buildId", ciRequest.WorkflowId) + + for time.Since(startTime) < maxWait { + if _, err := os.Stat(dockerfilePath); err == nil { + log.Println(util.DEVTRON, "dockerfile scan: Dockerfile found, proceeding", "path", dockerfilePath, "elapsed", time.Since(startTime).Round(time.Second)) + break // File exists, proceed + } + // Log progress every 30 seconds + if int(time.Since(startTime).Seconds())%30 == 0 { + log.Println(util.DEVTRON, "dockerfile scan: waiting for git clone to complete...", "path", dockerfilePath, "elapsed", time.Since(startTime).Round(time.Second), "maxWait", maxWait) + } + time.Sleep(waitInterval) + } + + // Read Dockerfile from filesystem (single source of truth) + dockerfileContent, err := os.ReadFile(dockerfilePath) + if err != nil { + log.Println(util.DEVTRON, "error in reading Dockerfile for scanning", + "path", dockerfilePath, "err", err) + if err := handleScanError(fmt.Sprintf("Failed to read Dockerfile from %s: %v", dockerfilePath, err), ciRequest.DockerfileScanEnabled); err != nil { + return + } + return + } + + // Prepare scan request with Dockerfile content + // CRITICAL FIX: Read DockerfileScanEnabled from ciRequest.DockerfileScanEnabled (CommonWorkflowRequest level) + // NOT from ciRequest.CiBuildConfig.DockerBuildConfig.DockerfileScanEnabled (which may be out of sync) + // ForceDockerfileScan is only available at CommonWorkflowRequest level + scanRequest := &DockerfileScanRequest{ + AppId: ciRequest.AppId, + BuildId: ciRequest.WorkflowId, + PipelineId: ciRequest.PipelineId, + DockerfileContent: string(dockerfileContent), + DockerfileScanEnabled: ciRequest.DockerfileScanEnabled, + ForceDockerfileScan: ciRequest.ForceDockerfileScan, + IgnoredRules: []string{}, // Can be populated from config in future + } + + jsonBody, err := json.Marshal(scanRequest) + if err != nil { + log.Println(util.DEVTRON, "error in marshalling Dockerfile scan request", "err", err) + if err := handleScanError(fmt.Sprintf("Failed to marshal scan request: %v", err), ciRequest.DockerfileScanEnabled); err != nil { + return + } + return + } + + cfg := &ScanConfig{} + err = env.Parse(cfg) + if err != nil { + log.Println(util.DEVTRON, "error in parsing scan config", "err", err) + if err := handleScanError(fmt.Sprintf("Failed to parse scan config: %v", err), ciRequest.DockerfileScanEnabled); err != nil { + return + } + return + } + + // Create HTTP client with timeout and configurable retries + client := resty.New() + client.SetTimeout(2 * time.Minute) + client. + SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true}). + SetRetryCount(cfg.MaxRetries). + SetRetryWaitTime(time.Duration(cfg.RetryWaitTimeSeconds) * time.Second) + + resp, err := client.R(). + SetHeader("Content-Type", "application/json"). + SetBody(jsonBody). + Post(fmt.Sprintf("%s/%s", cfg.ImageScannerEndpoint, "scanner/dockerfile/scan")) + + // Record success/failure in circuit breaker + if err != nil || (resp != nil && (resp.StatusCode() != http.StatusAccepted && resp.StatusCode() != http.StatusOK)) { + log.Println(util.DEVTRON, "circuit breaker recorded FAILURE", "buildId", ciRequest.WorkflowId) + } else { + log.Println(util.DEVTRON, "circuit breaker recorded SUCCESS", "buildId", ciRequest.WorkflowId) + } + + if err != nil { + log.Println(util.DEVTRON, "error in calling image-scanner for Dockerfile scan", "err", err) + if err := handleScanError(fmt.Sprintf("Dockerfile scan failed: %v", err), cfg.FailOnError); err != nil { + return + } + return + } + + // Accept both 202 (Accepted) and 200 (OK - for cached results) + if resp.StatusCode() != http.StatusAccepted && resp.StatusCode() != http.StatusOK { + log.Println(util.DEVTRON, "image-scanner returned non-202/200 status for Dockerfile scan", + "status", resp.StatusCode(), "body", string(resp.Body())) + if err := handleScanError(fmt.Sprintf("Dockerfile scan failed with status: %d", resp.StatusCode()), cfg.FailOnError); err != nil { + return + } + return + } + + log.Println(util.DEVTRON, "successfully initiated Dockerfile scan", + "statusCode", resp.StatusCode(), "buildId", ciRequest.WorkflowId) +} + +// handleScanError handles scan errors based on FailOnError configuration +func handleScanError(message string, failOnError bool) error { + if failOnError { + log.Println(util.DEVTRON, "Dockerfile scan failed (fail-on-error enabled)", "message", message) + return fmt.Errorf("Dockerfile scan failed: %s", message) + } + // Log warning but don't fail the build + log.Println(util.DEVTRON, "Dockerfile scan failed (fail-on-error disabled)", "message", message) + return nil +} diff --git a/ci-runner/helper/EventHelper.go b/ci-runner/helper/EventHelper.go index 29c652e13..cce459fc2 100644 --- a/ci-runner/helper/EventHelper.go +++ b/ci-runner/helper/EventHelper.go @@ -204,6 +204,8 @@ type CommonWorkflowRequest struct { AwsInspectorConfig string `json:"awsInspectorConfig,omitempty"` PartSize int64 `json:"partSize,omitempty"` ConcurrencyMultiplier int `json:"concurrencyMultiplier,omitempty"` + DockerfileScanEnabled bool `json:"dockerfileScanEnabled,omitempty"` + ForceDockerfileScan bool `json:"forceDockerfileScan,omitempty"` } func (c *CommonWorkflowRequest) IsPreCdStage() bool {