Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions .github/workflows/ci-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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 \
Expand Down Expand Up @@ -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 }})
Expand Down Expand Up @@ -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__"
Expand Down
31 changes: 18 additions & 13 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
225 changes: 225 additions & 0 deletions test/integration/sca_resolver_test.go
Original file line number Diff line number Diff line change
@@ -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 <path> ...` 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 <source-dir>/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")
}
3 changes: 1 addition & 2 deletions test/integration/scan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading