diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index 19b441f9..bc3a58de 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -34,7 +34,7 @@ jobs: - name: Find tests not covered by any named group id: find-uncovered run: | - # Combined regex of every pattern used across the 12 named matrix groups. + # Combined regex of every pattern used across the 13 named matrix groups. # Any test whose name does NOT match this will land in the catch-all group. # Built via concatenation so every line stays at ≥10-space YAML indentation. CP="TestCreateScan|TestScanCreate|TestScansE2E|TestFastScan" @@ -56,6 +56,7 @@ jobs: CP="${CP}|TestGitLab|TestBitbucket|TestBitBucket|TestAzure|TestHooksPreCommit" CP="${CP}|TestGetLearnMore|TestImport|TestGetTenant|TestMaskSecrets|TestFailedMask" CP="${CP}|TestScaRemediation|TestKicsRemediation|TestTelemetry|Test_Handle|TestChat" + CP="${CP}|TestIntegrationScaResolver" COVERED_PATTERNS="${CP}" ALL_TESTS=$(grep -rh "^func Test" test/integration/*_test.go \ @@ -84,8 +85,8 @@ jobs: fi # ───────────────────────────────────────────────────────────────────────────── - # Job B: Run each test group in parallel across 13 matrix entries. - # The 13th entry (uncovered) is a dynamic catch-all driven by Job A. + # Job B: Run each test group in parallel across 14 matrix entries. + # The 14th entry (uncovered) is a dynamic catch-all driven by Job A. # ───────────────────────────────────────────────────────────────────────────── integration-tests: name: Integration Tests (${{ matrix.label }}) @@ -191,7 +192,15 @@ jobs: needs_precommit: "true" run_cleandata: "false" - # 13 ── Catch-All (dynamic; pattern injected at runtime from Job A output) + # 13 ── SCA Resolver (sca_resolver_test.go only; isolated so the real ~114MB ScaResolver download + real scans run exactly once) + - name: sca-resolver + label: "SCA Resolver" + run_pattern: "TestIntegrationScaResolver" + timeout: "45m" + needs_precommit: "false" + run_cleandata: "true" + + # 14 ── Catch-All (dynamic; pattern injected at runtime from Job A output) - name: uncovered label: "Catch-All (Uncovered)" run_pattern: "__UNCOVERED__" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0d512844..806ec9fb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -71,17 +71,10 @@ jobs: build: name: Build, Sign & Publish Release runs-on: cx-public-macos-15-x64 + environment: release permissions: - id-token: write # required for AWS OIDC federation via configure-aws-credentials (HSM signing) + id-token: write # required for AWS OIDC federation via configure-aws-credentials (HSM signing + Secrets Manager) contents: write # required for `gh release create`/`gh release delete` to manage GitHub Releases - env: - AC_PASSWORD: ${{ secrets.AC_PASSWORD }} - AC_USER: ${{ secrets.AC_USER }} - APPLE_DEVELOPER_CERTIFICATE_P12_BASE64: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_P12_BASE64 }} - APPLE_DEVELOPER_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_PASSWORD }} - COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }} - COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }} - COSIGN_PUBLIC_KEY: ${{ secrets.COSIGN_PUBLIC_KEY }} steps: - name: Install Harden Runner uses: checkmarx/harden-runner-action@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 @@ -97,13 +90,25 @@ jobs: uses: actions/setup-go@4d34df0c2316fe8122ab82dc22947d607c0c91f9 #v4 with: go-version-file: go.mod + - name: Configure AWS Credentials for Secrets Manager + uses: step-security/configure-aws-credentials@1448016543d4432bd608e0de7871c7bf9e9be925 # v6.2.1 + with: + role-to-assume: ${{ secrets.SECRET_MANAGER_ROLE_ARN }} + aws-region: ${{ secrets.SECRET_MANAGER_AWS_REGION }} + role-session-name: release-secrets-fetch + - name: Fetch secrets from AWS Secrets Manager + uses: step-security/aws-secretsmanager-get-secrets@102b6b2a2528747bcc321bb99ef326d8be7ef58f # v3.0.1 + with: + secret-ids: | + ${{ secrets.SECRET_MANAGER_SECRET_NAME }} + parse-json-secrets: true - name: Import Code-Signing Certificates uses: Apple-Actions/import-codesign-certs@253ddeeac23f2bdad1646faac5c8c2832e800071 #v1 with: # The certificates in a PKCS12 file encoded as a base64 string - p12-file-base64: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_P12_BASE64 }} + p12-file-base64: ${{ env.APPLE_DEVELOPER_CERTIFICATE_P12_BASE64 }} # The password used to import the PKCS12 file. - p12-password: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_PASSWORD }} + p12-password: ${{ env.APPLE_DEVELOPER_CERTIFICATE_PASSWORD }} - name: Updating and upgrading brew to a specific version run: | brew --version @@ -132,8 +137,8 @@ jobs: if: inputs.dev == false uses: step-security/docker-login-action@870af644803bf9f204aed474adbad2958fec048b # v4.1.0 with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} + username: ${{ env.DOCKER_USERNAME }} + password: ${{ env.DOCKER_PASSWORD }} - name: Install Cosign if: inputs.dev == false diff --git a/test/integration/sca_resolver_test.go b/test/integration/sca_resolver_test.go new file mode 100644 index 00000000..08032891 --- /dev/null +++ b/test/integration/sca_resolver_test.go @@ -0,0 +1,225 @@ +//go:build integration + +package integration + +import ( + "bytes" + "log" + "os" + "path/filepath" + "strings" + "sync" + "testing" + + "github.com/checkmarx/ast-cli/internal/commands/scarealtime/scaconfig" + "github.com/checkmarx/ast-cli/internal/params" + "github.com/checkmarx/ast-cli/internal/services/osinstaller" + "gotest.tools/assert" +) + +// Separate from scaconfig.Params.WorkingDirName so it never touches the sca-realtime tests' cache dir. +const scaResolverWorkingDirName = "SCAResolverIntegrationTest" + +var ( + scaResolverOnce sync.Once + scaResolverConfig osinstaller.InstallationConfiguration + scaResolverPath string + scaResolverErr error +) + +// getScaResolverExecutable downloads the real ScaResolver executable once and shares it +// across every test in this file, so the ~114MB download only happens a single time. +func getScaResolverExecutable(t *testing.T) string { + scaResolverOnce.Do(func() { + scaResolverConfig = scaconfig.Params + scaResolverConfig.WorkingDirName = scaResolverWorkingDirName + + _, scaResolverErr = osinstaller.InstallOrUpgrade(&scaResolverConfig, nil) + if scaResolverErr == nil { + scaResolverPath = scaResolverConfig.ExecutableFilePath() + } + }) + + if scaResolverErr != nil { + t.Fatalf("Failed to download ScaResolver executable: %v", scaResolverErr) + } + + return scaResolverPath +} + +// TestIntegrationScaResolverExecutable_Success runs a real +// `cx scan create --sca-resolver ...` using the actual downloaded ScaResolver +// executable, rather than a path pre-staged outside the test. +func TestIntegrationScaResolverExecutable_Success(t *testing.T) { + resolverPath := getScaResolverExecutable(t) + + args := []string{ + "scan", "create", + flag(params.ProjectName), getProjectNameForScanTests(), + flag(params.SourcesFlag), Dir, + flag(params.ScaResolverFlag), resolverPath, + flag(params.ScaResolverParamsFlag), "-q", + flag(params.ScanTypes), "sca", + flag(params.BranchFlag), "dummy_branch", + flag(params.DebugFlag), + } + + err, _ := executeCommand(t, args...) + assert.NilError(t, err) +} + +// Test --no-scan without --sbom-first (in --sca-resolver-params) is rejected with the +// bad-use error and no scan is submitted. +func TestIntegrationScaResolverNoScanWithoutSbomFirst(t *testing.T) { + args := []string{ + "scan", "create", + flag(params.ProjectName), getProjectNameForScanTests(), + flag(params.SourcesFlag), Dir, + flag(params.BranchFlag), "dummy_branch", + flag(params.NoScanFlag), + } + + err, _ := executeCommand(t, args...) + assertError( + t, + err, + "--no-scan flag was passed without --sbom-first: No SBOM was generated and the CxOne scan was skipped. "+ + "Submit --sbom-first under --sca-resolver-params to generate an SBOM.", + ) +} + +// --no-scan + --sbom-first (default location): SBOM saved to /cx-sbom.json, no scan submitted. +func TestIntegrationScaResolverNoScanWithSbomFirst_DefaultLocation(t *testing.T) { + resolverPath := getScaResolverExecutable(t) + + absDir, absErr := filepath.Abs(Dir) + assert.NilError(t, absErr) + expectedSbomPath := filepath.Clean(filepath.Join(absDir, "cx-sbom.json")) + defer func() { _ = os.Remove(expectedSbomPath) }() + + args := []string{ + "scan", "create", + flag(params.ProjectName), getProjectNameForScanTests(), + flag(params.SourcesFlag), Dir, + flag(params.ScaResolverFlag), resolverPath, + flag(params.ScaResolverParamsFlag), "--sbom-first", + flag(params.ScanTypes), "sca", + flag(params.BranchFlag), "dummy_branch", + flag(params.NoScanFlag), + flag(params.DebugFlag), + } + err, _ := executeCommand(t, args...) + assert.NilError(t, err, "scan create with --no-scan + --sbom-first (default location) should succeed") +} + +// --no-scan + --sbom-first with custom --sbom-output-path/--sbom-output-name: SBOM saved to the custom location, no scan submitted. +func TestIntegrationScaResolverNoScanWithSbomFirst_CustomOutputPathAndName(t *testing.T) { + resolverPath := getScaResolverExecutable(t) + + // Subdir under the source dir, not t.TempDir(), to avoid OS temp-folder quirks. + absSourceDir, absErr := filepath.Abs(Dir) + assert.NilError(t, absErr) + outputDir := filepath.Join(absSourceDir, "custom-sbom-output") + assert.NilError(t, os.MkdirAll(outputDir, 0o755)) + defer func() { _ = os.RemoveAll(outputDir) }() + + const customSbomName = "my-project-sbom.json" + expectedSbomPath := filepath.Clean(filepath.Join(outputDir, customSbomName)) + + args := []string{ + "scan", "create", + flag(params.ProjectName), getProjectNameForScanTests(), + flag(params.SourcesFlag), Dir, + flag(params.ScaResolverFlag), resolverPath, + flag(params.ScaResolverParamsFlag), + "--sbom-first --sbom-output-path " + outputDir + " --sbom-output-name " + customSbomName, + flag(params.ScanTypes), "sca", + flag(params.BranchFlag), "dummy_branch", + flag(params.NoScanFlag), + flag(params.DebugFlag), + } + + var buf bytes.Buffer + log.SetOutput(&buf) + defer func() { + log.SetOutput(os.Stderr) + }() + + err, _ := executeCommand(t, args...) + assert.NilError(t, err, "scan create with --no-scan + custom sbom output path/name should succeed") + + logText := buf.String() + assert.Assert( + t, + strings.Contains(logText, "Resolved packages information was saved"), + "Expected ScaResolver success message not found in logs", + ) + assert.Assert( + t, + strings.Contains(logText, "SBOM generated and saved to: "+expectedSbomPath), + "Expected SBOM generation confirmation at the custom path/name not found in logs", + ) + assert.Assert( + t, + strings.Contains(logText, "--no-scan set: skipping source compression and upload."), + "Expected source compression/upload skip message not found in logs", + ) + assert.Assert( + t, + strings.Contains(logText, "--no-scan set: skipping scan submission."), + "Expected scan submission skip message not found in logs", + ) + + _, statErr := os.Stat(expectedSbomPath) + assert.NilError(t, statErr, "SBOM file should actually exist at the custom output path/name on disk") +} + +// --sbom-first without --no-scan: SBOM is generated and the CxOne scan still runs normally. +func TestIntegrationScaResolverSbomFirstWithoutNoScan(t *testing.T) { + resolverPath := getScaResolverExecutable(t) + + absDir, absErr := filepath.Abs(Dir) + assert.NilError(t, absErr) + expectedSbomPath := filepath.Clean(filepath.Join(absDir, "cx-sbom.json")) + defer func() { _ = os.Remove(expectedSbomPath) }() + + args := []string{ + "scan", "create", + flag(params.ProjectName), getProjectNameForScanTests(), + flag(params.SourcesFlag), Dir, + flag(params.ScaResolverFlag), resolverPath, + flag(params.ScaResolverParamsFlag), "--sbom-first", + flag(params.ScanTypes), "sca", + flag(params.BranchFlag), "dummy_branch", + flag(params.DebugFlag), + } + + var buf bytes.Buffer + log.SetOutput(&buf) + defer func() { + log.SetOutput(os.Stderr) + }() + + err, _ := executeCommand(t, args...) + assert.NilError(t, err, "scan create with --sbom-first (no --no-scan) should succeed and submit a scan") + + logText := buf.String() + assert.Assert( + t, + strings.Contains(logText, "Resolved packages information was saved"), + "Expected ScaResolver success message not found in logs", + ) + assert.Assert( + t, + strings.Contains(logText, "SBOM generated and saved to: "+expectedSbomPath), + "Expected SBOM generation confirmation not found in logs", + ) + assert.Assert( + t, + !strings.Contains(logText, "--no-scan set"), + "scan submission/upload should NOT be skipped when --no-scan is not passed", + ) + + _, statErr := os.Stat(expectedSbomPath) + assert.NilError(t, statErr, "SBOM file should actually exist on disk even though the scan was also submitted") +} diff --git a/test/integration/scan_test.go b/test/integration/scan_test.go index ad25fe3a..cc4f1371 100644 --- a/test/integration/scan_test.go +++ b/test/integration/scan_test.go @@ -1375,8 +1375,7 @@ func TestRunKicsScanWithAdditionalParams(t *testing.T) { } func TestRunScaRealtimeScan(t *testing.T) { - t.Skip("Skip this test cases due to context deadline exceeded") - args := []string{scanCommand, "sca-realtime", "--project-dir", projectDirectory} + args := []string{scanCommand, "sca-realtime", "--project-dir", projectDirectory, flag(params.DebugFlag)} err, _ := executeCommand(t, args...) assert.NilError(t, err)