diff --git a/.github/workflows/sonarIntegrationTests.yml b/.github/workflows/sonarIntegrationTests.yml new file mode 100644 index 000000000..5ab4164e7 --- /dev/null +++ b/.github/workflows/sonarIntegrationTests.yml @@ -0,0 +1,84 @@ +name: SonarQube Integration Tests +on: + workflow_dispatch: + push: + # TODO - Remove this branch filter once the spike is complete. + branches: [ sonar-evd-spike ] + +jobs: + test-jfrog-sonar: + runs-on: ubuntu-latest + services: + sonar: + image: sonarqube:community + ports: + - 9000:9000 + options: >- + --health-cmd="curl --fail -uadmin:admin http://localhost:9000/api/system/health || exit 1" + --health-interval=10s + --health-timeout=5s + --health-retries=30 + env: + SONAR_ES_BOOTSTRAP_CHECKS_DISABLE: "true" + + steps: + - name: Checkout Code + uses: actions/checkout@v3 + with: + ref: ${{ github.event.pull_request.head.sha }} + + - name: Fetch Sonar Access Token + id: sonar_token + run: | + echo "Fetching SonarQube access token..." + TOKEN=$(curl -s -X POST -u "admin:admin" \ + "http://localhost:9000/api/user_tokens/generate?name=github-actions-token" | jq -r '.token') + echo "SONARQUBE_TOKEN=${TOKEN}" >> $GITHUB_ENV + echo "JF_SONARQUBE_ACCESS_TOKEN=${TOKEN}" >> $GITHUB_ENV + + - name: Create Project in SonarQube + run: | + echo "Creating SonarQube project..." + curl -u "admin:admin" -X POST "http://localhost:9000/api/projects/create?name=mvn-sonar&project=mvn-sonar" + + - name: Install JFrog CLI manually + run: | + curl -fL https://install-cli.jfrog.io | sh + + - name: Configure JFrog CLI + run: | + jf c add artifactory-server \ + --url ${{ secrets.PLATFORM_URL }} \ + --user ${{ secrets.PLATFORM_USER }} \ + --access-token ${{ secrets.PLATFORM_ADMIN_TOKEN }} \ + --interactive=false + + - name: Set up Java + uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-version: '17' + + - name: Run SonarQube Analysis with JFrog CLI + working-directory: testdata/maven/mavenprojectwithsonar + run: | + echo "Running SonarQube analysis..." + jf mvn clean verify install sonar:sonar \ + -Dsonar.projectKey=mvn-sonar \ + -Dsonar.projectName='mvn-sonar' \ + -Dsonar.host.url=http://localhost:9000 \ + -Dsonar.token=${SONARQUBE_TOKEN} + + - name: Run sonar integration tests + env: + JF_SONARQUBE_ACCESS_TOKEN: ${{ env.SONARQUBE_TOKEN }} + PLATFORM_URL: ${{ secrets.PLATFORM_URL }} + PLATFORM_API_KEY: ${{ secrets.PLATFORM_ADMIN_TOKEN }} + run: go test -v -run "TestSonar" github.com/jfrog/jfrog-cli --timeout 0 --test.sonarIntegration --jfrog.url=${{ secrets.PLATFORM_URL }} --jfrog.adminToken=${{ secrets.PLATFORM_ADMIN_TOKEN }} + + - name: Clean up + if: always() + run: | + echo "Cleaning up generated artifacts and maven packages..." + jf rt del "dev-maven-local/com/example/demo-sonar/1.0*" --recursive --fail-no-op + jf rt bdi test-sonar-jf-cli-integration diff --git a/artifactory_test.go b/artifactory_test.go index d9e618fbf..bd55f4da5 100644 --- a/artifactory_test.go +++ b/artifactory_test.go @@ -227,6 +227,7 @@ func TestArtifactorySimpleUploadSpecUsingConfig(t *testing.T) { } func TestReleaseBundleImportOnPrem(t *testing.T) { + initArtifactoryTest(t, "") // Cleanup defer func() { deleteReceivedReleaseBundle(t, deleteReleaseBundleV1ApiUrl, "cli-tests", "2") @@ -244,6 +245,7 @@ func TestReleaseBundleImportOnPrem(t *testing.T) { } func TestReleaseBundleV2Download(t *testing.T) { + initArtifactoryTest(t, "") buildNumber := "5" defer func() { deleteReceivedReleaseBundle(t, deleteReleaseBundleV2ApiUrl, tests.LcRbName1, buildNumber) diff --git a/go.mod b/go.mod index 34b98263e..12e6c2616 100644 --- a/go.mod +++ b/go.mod @@ -16,13 +16,13 @@ require ( github.com/docker/docker v27.5.1+incompatible github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1 github.com/jfrog/archiver/v3 v3.6.1 - github.com/jfrog/build-info-go v1.10.10 + github.com/jfrog/build-info-go v1.10.11 github.com/jfrog/gofrog v1.7.6 github.com/jfrog/jfrog-cli-artifactory v0.2.1 - github.com/jfrog/jfrog-cli-core/v2 v2.58.2 + github.com/jfrog/jfrog-cli-core/v2 v2.58.3 github.com/jfrog/jfrog-cli-platform-services v1.9.0 github.com/jfrog/jfrog-cli-security v1.16.2 - github.com/jfrog/jfrog-client-go v1.51.1 + github.com/jfrog/jfrog-client-go v1.52.0 github.com/jszwec/csvutil v1.10.0 github.com/manifoldco/promptui v0.9.0 github.com/stretchr/testify v1.10.0 @@ -191,8 +191,8 @@ require ( replace github.com/jfrog/jfrog-cli-core/v2 => github.com/jfrog/jfrog-cli-core/v2 v2.31.1-0.20250410085750-f34f5feea93e -replace github.com/jfrog/jfrog-client-go => github.com/jfrog/jfrog-client-go v1.28.1-0.20250406105605-ee90d11546f9 +replace github.com/jfrog/jfrog-client-go => github.com/bhanurp/jfrog-client-go v1.28.1-0.20250608133457-6a4cfafe1865 -replace github.com/jfrog/jfrog-cli-artifactory => github.com/jfrog/jfrog-cli-artifactory v0.2.2-0.20250414045808-41544959f9b9 +replace github.com/jfrog/jfrog-cli-artifactory => github.com/bhanurp/jfrog-cli-artifactory v0.1.12-0.20250622193359-8ebe3a10c43f replace github.com/jfrog/jfrog-cli-security => github.com/jfrog/jfrog-cli-security v1.16.3-0.20250402121228-12cce9f88504 diff --git a/go.sum b/go.sum index d4adfac5b..467f90c50 100644 --- a/go.sum +++ b/go.sum @@ -34,6 +34,10 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/beevik/etree v1.4.0 h1:oz1UedHRepuY3p4N5OjE0nK1WLCqtzHf25bxplKOHLs= github.com/beevik/etree v1.4.0/go.mod h1:cyWiXwGoasx60gHvtnEh5x8+uIjUVnjWqBvEnhnqKDA= +github.com/bhanurp/jfrog-cli-artifactory v0.1.12-0.20250622193359-8ebe3a10c43f h1:u45tgidRfRI5OeNVDW4F79PyvZv2USvIAW+OWgL34JI= +github.com/bhanurp/jfrog-cli-artifactory v0.1.12-0.20250622193359-8ebe3a10c43f/go.mod h1:34yyDLWQSYzxiG4AO3GUfwMk/VVblnYGAZwmCMaPDM0= +github.com/bhanurp/jfrog-client-go v1.28.1-0.20250608133457-6a4cfafe1865 h1:kilH1D7qR3aOv+pEfC1ErirRFiNXnYdYIwp01XLOvaI= +github.com/bhanurp/jfrog-client-go v1.28.1-0.20250608133457-6a4cfafe1865/go.mod h1:uRmT8Q1SJymIzId01v0W1o8mGqrRfrwUF53CgEMsH0U= github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M= github.com/bradleyjkemp/cupaloy/v2 v2.8.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0= github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= @@ -176,8 +180,8 @@ github.com/jedib0t/go-pretty/v6 v6.6.5 h1:9PgMJOVBedpgYLI56jQRJYqngxYAAzfEUua+3N github.com/jedib0t/go-pretty/v6 v6.6.5/go.mod h1:Uq/HrbhuFty5WSVNfjpQQe47x16RwVGXIveNGEyGtHs= github.com/jfrog/archiver/v3 v3.6.1 h1:LOxnkw9pOn45DzCbZNFV6K0+6dCsQ0L8mR3ZcujO5eI= github.com/jfrog/archiver/v3 v3.6.1/go.mod h1:VgR+3WZS4N+i9FaDwLZbq+jeU4B4zctXL+gL4EMzfLw= -github.com/jfrog/build-info-go v1.10.10 h1:2nOFjV7SX1uisi2rQK7fb4Evm7YkSOdmssrm6Tf4ipc= -github.com/jfrog/build-info-go v1.10.10/go.mod h1:JcISnovFXKx3wWf3p1fcMmlPdt6adxScXvoJN4WXqIE= +github.com/jfrog/build-info-go v1.10.11 h1:wAMGCAHa49+ec01HqzSidLAHNIub+glh4ksFp3pYy7o= +github.com/jfrog/build-info-go v1.10.11/go.mod h1:JcISnovFXKx3wWf3p1fcMmlPdt6adxScXvoJN4WXqIE= github.com/jfrog/froggit-go v1.16.2 h1:F//S83iXH14qsCwYzv0zB2JtjS2pJVEsUoEmYA+37dQ= github.com/jfrog/froggit-go v1.16.2/go.mod h1:5VpdQfAcbuyFl9x/x8HGm7kVk719kEtW/8YJFvKcHPA= github.com/jfrog/go-mockhttp v0.3.1 h1:/wac8v4GMZx62viZmv4wazB5GNKs+GxawuS1u3maJH8= @@ -186,16 +190,12 @@ github.com/jfrog/gofrog v1.7.6 h1:QmfAiRzVyaI7JYGsB7cxfAJePAZTzFz0gRWZSE27c6s= github.com/jfrog/gofrog v1.7.6/go.mod h1:ntr1txqNOZtHplmaNd7rS4f8jpA5Apx8em70oYEe7+4= github.com/jfrog/jfrog-apps-config v1.0.1 h1:mtv6k7g8A8BVhlHGlSveapqf4mJfonwvXYLipdsOFMY= github.com/jfrog/jfrog-apps-config v1.0.1/go.mod h1:8AIIr1oY9JuH5dylz2S6f8Ym2MaadPLR6noCBO4C22w= -github.com/jfrog/jfrog-cli-artifactory v0.2.2-0.20250414045808-41544959f9b9 h1:j9bepUA23952AdytsBqGbsl4QMScksbCFXulqWvj0eY= -github.com/jfrog/jfrog-cli-artifactory v0.2.2-0.20250414045808-41544959f9b9/go.mod h1:8qrGaRb162a4NWGr7R1rj8P80s8NU8KRTs69NMkQENA= github.com/jfrog/jfrog-cli-core/v2 v2.31.1-0.20250410085750-f34f5feea93e h1:N+7gJdZmwggKqrTbrEvAFxxXQziFbJ4zHI/sXa8vR1A= github.com/jfrog/jfrog-cli-core/v2 v2.31.1-0.20250410085750-f34f5feea93e/go.mod h1:4S7yztLwWq4yA+k9j9s5gvIqr7xC/6EjJQ+0ENCHTFc= github.com/jfrog/jfrog-cli-platform-services v1.9.0 h1:r/ETgJuMUOUu12w20ydsF6paqEaj0khH6bxMRsdNz1Y= github.com/jfrog/jfrog-cli-platform-services v1.9.0/go.mod h1:pMZMSwhj7yA4VKyj0Skr2lObIyGpZUxNJ40DSLKXU38= github.com/jfrog/jfrog-cli-security v1.16.3-0.20250402121228-12cce9f88504 h1:mnU8PtDaCmU1ZC8Wcy0VKj1gJEZnnyjgAc3rJLCcMjs= github.com/jfrog/jfrog-cli-security v1.16.3-0.20250402121228-12cce9f88504/go.mod h1:tJyLh4KI4qoF/AVBy0wC9s8DVxV/hoyKK4LIzpxL590= -github.com/jfrog/jfrog-client-go v1.28.1-0.20250406105605-ee90d11546f9 h1:pEBTHYeyuDa+w0oJNCYFq1wD2O2NqWdDTAtDRFy7s3w= -github.com/jfrog/jfrog-client-go v1.28.1-0.20250406105605-ee90d11546f9/go.mod h1:uRmT8Q1SJymIzId01v0W1o8mGqrRfrwUF53CgEMsH0U= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jszwec/csvutil v1.10.0 h1:upMDUxhQKqZ5ZDCs/wy+8Kib8rZR8I8lOR34yJkdqhI= diff --git a/sonarintegration_test.go b/sonarintegration_test.go new file mode 100644 index 000000000..b4b2508ba --- /dev/null +++ b/sonarintegration_test.go @@ -0,0 +1,500 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + configUtils "github.com/jfrog/jfrog-cli-core/v2/utils/config" + coreTests "github.com/jfrog/jfrog-cli-core/v2/utils/tests" + "github.com/jfrog/jfrog-cli/utils/tests" + clientUtils "github.com/jfrog/jfrog-client-go/utils" + "github.com/stretchr/testify/assert" + "io" + "net/http" + "os" + "path/filepath" + "strings" + "testing" + "time" +) + +var ( + sonarIntegrationCLI *coreTests.JfrogCli + rtCLI *coreTests.JfrogCli + configCLI *coreTests.JfrogCli + evidenceDetails *configUtils.ServerDetails +) + +type KeyPair struct { + PairName string `json:"pairName"` + PairType string `json:"pairType"` + Alias string `json:"alias"` + PrivateKey string `json:"privateKey"` + PublicKey string `json:"publicKey"` +} + +type EvidenceResponse struct { + Data Data `json:"data"` +} +type Node struct { + Name string `json:"name"` + Path string `json:"path"` + RepositoryKey string `json:"repositoryKey"` + DownloadPath string `json:"downloadPath"` + Sha256 string `json:"sha256"` + PredicateType string `json:"predicateType"` + CreatedAt time.Time `json:"createdAt"` + CreatedBy string `json:"createdBy"` + Verified bool `json:"verified"` + PredicateSlug string `json:"predicateSlug"` +} +type Edges struct { + Node Node `json:"node"` +} +type SearchEvidence struct { + Edges []Edges `json:"edges"` +} +type Evidence struct { + SearchEvidence SearchEvidence `json:"searchEvidence"` +} +type Data struct { + Evidence Evidence `json:"evidence"` +} + +const ( + KeyPairAlias = "evidence-local" + keyPairName = "test-signing-key" + buildName = "test-sonar-jf-cli-integration" +) + +func initSonarCli() { + if sonarIntegrationCLI != nil { + return + } + sonarIntegrationCLI = coreTests.NewJfrogCli(execMain, "jfrog", authenticateEvidence()) +} + +func initSonarCliForBuildPublish() { + if rtCLI != nil { + return + } + flags := authenticateEvidence() + rtCLI = coreTests.NewJfrogCli(execMain, "jfrog", strings.TrimSpace(flags)) + configCLI = coreTests.NewJfrogCli(execMain, "jfrog", "") +} + +func initSonarIntegrationTest(t *testing.T) { + if !*tests.TestSonar { + t.Skip("Skipping Access test. To run Access test add the '-test.sonarIntegration=true' option.") + } + if os.Getenv("JF_SONARQUBE_ACCESS_TOKEN") == "" { + t.Fatal("JF_SONARQUBE_ACCESS_TOKEN environment variable is not set. Please set it to run the SonarQube integration test.") + } +} + +func TestSonarPrerequisites(t *testing.T) { + initSonarIntegrationTest(t) + reportFilePath := "testdata/maven/mavenprojectwithsonar/target/sonar/report-task.txt" + if _, err := os.Stat(reportFilePath); os.IsNotExist(err) { + t.Fatalf("Failed to find file %s", reportFilePath) + } + fileContent, err := os.ReadFile(reportFilePath) + if err != nil { + t.Fatalf("Failed to read file %s: %v", reportFilePath, err) + } + isCeTaskUrlFound := false + sonarURL := "" + for _, line := range strings.Split(string(fileContent), "\n") { + if strings.HasPrefix(line, "ceTaskUrl=") { + isCeTaskUrlFound = true + sonarURL = strings.TrimPrefix(line, "ceTaskUrl=") + break + } + } + assert.True(t, isCeTaskUrlFound, "File %s does not contain 'ceTaskUrl='", reportFilePath) + assert.NotEmpty(t, "File %s does not contain a valid SonarQube URL", reportFilePath) + assert.True(t, strings.HasPrefix(sonarURL, "http://localhost:9000/api/ce/task?id="), "SonarQube URL is not valid: %s", sonarURL) + taskID := strings.TrimPrefix(sonarURL, "http://localhost:9000/api/ce/task?id=") + assert.NotEmpty(t, taskID, "task ID should not be empty") + resp, err := http.Get("http://localhost:9000/api/system/status") + if err != nil { + t.Fatalf("Failed to connect to SonarQube server: %v", err) + } + assert.Equal(t, resp.StatusCode, http.StatusOK, "SonarQube server is not running or returned an unexpected status code") + // Check if given sonar_access_token is valid + sonarAccessToken := os.Getenv("JF_SONARQUBE_ACCESS_TOKEN") + if sonarAccessToken == "" { + t.Fatal("JF_SONARQUBE_ACCESS_TOKEN environment variable is not set. Please set it to run the SonarQube integration test.") + } + // use sonarAccessToken to authenticate with SonarQube + req, err := http.NewRequest("GET", "http://localhost:9000/api/authentication/validate", nil) + req.Header.Set("Authorization", "Bearer "+sonarAccessToken) + client := &http.Client{} + resp, err = client.Do(req) + assert.NoError(t, err, "Failed to validate SonarQube access token") +} + +func TestSonarIntegrationAsEvidence(t *testing.T) { + initSonarCli() + initSonarIntegrationTest(t) + privateKeyFilePath := KeyPairGenerationAndUpload(t) + err := os.Setenv("JFROG_CLI_LOG_LEVEL", "DEBUG") + assert.NoError(t, err) + defer os.Unsetenv("JFROG_CLI_LOG_LEVEL") + // Change to the directory containing the Maven project and execute cli command + origDir, err := os.Getwd() + assert.NoError(t, err) + newDir := "testdata/maven/mavenprojectwithsonar" + err = os.Chdir(newDir) + assert.NoError(t, err) + defer func() { + err := os.Chdir(origDir) + assert.NoError(t, err) + }() + + output := sonarIntegrationCLI.RunCliCmdWithOutput(t, "evd", "create", "--predicate-type=sonar", + "--package-name=demo-sonar", "--package-version=1.0", "--package-repo-name=dev-maven-local", + "--key-alias="+keyPairName, "--key="+privateKeyFilePath) + assert.Empty(t, output) + evidenceResponseBytes, err := FetchEvidenceFromArtifactory(t, *tests.JfrogUrl, *tests.JfrogAccessToken, "dev-maven-local", "demo-sonar", "1.0") + assert.NoError(t, err) + // Unmarshal the response into EvidenceResponse struct + var evidenceResponse EvidenceResponse + err = json.Unmarshal(evidenceResponseBytes, &evidenceResponse) + assert.NoError(t, err) + assert.Equal(t, 1, len(evidenceResponse.Data.Evidence.SearchEvidence.Edges)) + assert.Equal(t, evidenceResponse.Data.Evidence.SearchEvidence.Edges[0].Node.Path, "com/example/demo-sonar/1.0/demo-sonar-1.0.pom") +} + +func TestSonarIntegrationAsEvidenceWithZeroConfig(t *testing.T) { + initSonarCli() + initSonarIntegrationTest(t) + privateKeyFilePath := KeyPairGenerationAndUpload(t) + err := os.Setenv("JFROG_CLI_LOG_LEVEL", "DEBUG") + assert.NoError(t, err) + defer os.Unsetenv("JFROG_CLI_LOG_LEVEL") + // Change to the directory containing the Maven project and execute cli command + origDir, err := os.Getwd() + assert.NoError(t, err) + newDir := "testdata/maven/mavenprojectwithsonar" + err = os.Chdir(newDir) + assert.NoError(t, err) + defer func() { + err := os.Chdir(origDir) + assert.NoError(t, err) + }() + // Remove evidence configuration so that the zero config will be used + evidenceDir := filepath.Join(".jfrog", "evidence") + err = os.RemoveAll(evidenceDir) + assert.NoError(t, err) + output := sonarIntegrationCLI.RunCliCmdWithOutput(t, "evd", "create", "--predicate-type=sonar", + "--package-name=demo-sonar", "--package-version=1.0", "--package-repo-name=dev-maven-local", + "--key-alias="+keyPairName, "--key="+privateKeyFilePath) + assert.Empty(t, output) + evidenceResponseBytes, err := FetchEvidenceFromArtifactory(t, *tests.JfrogUrl, *tests.JfrogAccessToken, "dev-maven-local", "demo-sonar", "1.0") + assert.NoError(t, err) + // Unmarshal the response into EvidenceResponse struct + var evidenceResponse EvidenceResponse + err = json.Unmarshal(evidenceResponseBytes, &evidenceResponse) + assert.NoError(t, err) + assert.Equal(t, 2, len(evidenceResponse.Data.Evidence.SearchEvidence.Edges)) + assert.Equal(t, evidenceResponse.Data.Evidence.SearchEvidence.Edges[1].Node.Path, "com/example/demo-sonar/1.0/demo-sonar-1.0.pom") +} + +func TestSonarIntegrationEvidenceCollectionWithBuildPublish(t *testing.T) { + initSonarCliForBuildPublish() + initSonarIntegrationTest(t) + err := os.Setenv("JFROG_CLI_LOG_LEVEL", "DEBUG") + assert.NoError(t, err) + defer os.Unsetenv("JFROG_CLI_LOG_LEVEL") + origDir, err := os.Getwd() + assert.NoError(t, err) + newDir := "testdata/maven/mavenprojectwithsonar" + err = os.Chdir(newDir) + assert.NoError(t, err) + defer func() { + err := os.Chdir(origDir) + assert.NoError(t, err) + }() + evidenceDetails.ArtifactoryUrl = *tests.JfrogUrl + "artifactory/" + evidenceDetails.Url = *tests.JfrogUrl + copyEvidenceYaml(t) + rtCLI.WithoutCredentials().RunCliCmdWithOutput(t, + "rt", + "bp", + buildName, + "1", + ) + evidenceResponseBytes, err := FetchEvidenceFromArtifactory(t, *tests.JfrogUrl, *tests.JfrogAccessToken, "artifactory-build-info", "demo-sonar", "1.0") + assert.NoError(t, err) + var evidenceResponse EvidenceResponse + err = json.Unmarshal(evidenceResponseBytes, &evidenceResponse) + assert.NoError(t, err) + latestBuildInfo := len(evidenceResponse.Data.Evidence.SearchEvidence.Edges) + assert.True(t, strings.HasPrefix(evidenceResponse.Data.Evidence.SearchEvidence.Edges[latestBuildInfo-1].Node.Path, "test-sonar-jf-cli-integration/1")) +} + +func authenticateEvidence() string { + *tests.JfrogUrl = clientUtils.AddTrailingSlashIfNeeded(*tests.JfrogUrl) + evidenceDetails = &configUtils.ServerDetails{ + Url: *tests.JfrogUrl, + } + var cred string + cred = fmt.Sprintf("--url=%s", *tests.JfrogUrl) + if *tests.JfrogAccessToken != "" { + evidenceDetails.AccessToken = *tests.JfrogAccessToken + cred = fmt.Sprintf("%s --access-token=%s", cred, evidenceDetails.AccessToken) + } else { + evidenceDetails.User = *tests.JfrogUser + evidenceDetails.Password = *tests.JfrogPassword + if cred != "" { + cred = fmt.Sprintf("%s --user=%s --password=%s", cred, evidenceDetails.User, evidenceDetails.Password) + } else { + cred = fmt.Sprintf("--user=%s --password=%s", evidenceDetails.User, evidenceDetails.Password) + } + } + return cred +} + +func copyEvidenceYaml(t *testing.T) { + src := "evidence.yaml" + dstDir := filepath.Join(".jfrog", "evidence") + dst := filepath.Join(dstDir, "evidence.yaml") + + err := os.MkdirAll(dstDir, 0755) + if err != nil { + t.Fatalf("Failed to create directory %s: %v", dstDir, err) + } + + srcFile, err := os.Open(src) + if err != nil { + t.Fatalf("Failed to open source file: %v", err) + } + defer srcFile.Close() + + dstFile, err := os.Create(dst) + if err != nil { + t.Fatalf("Failed to create destination file: %v", err) + } + defer dstFile.Close() + + _, err = io.Copy(dstFile, srcFile) + if err != nil { + t.Fatalf("Failed to copy file: %v", err) + } +} + +// KeyPairGenerationAndUpload Deletes the existing signing key from Artifactory, +// generates a new RSA key pair, and uploads it to Artifactory. +func KeyPairGenerationAndUpload(t *testing.T) string { + artifactoryURL := os.Getenv("PLATFORM_URL") + apiKey := os.Getenv("PLATFORM_API_KEY") + assert.NotEmpty(t, artifactoryURL) + assert.NotEmpty(t, apiKey, "PLATFORM_API_KEY should not be empty") + privateKeyFilePath, publicKeyFilePath, err := generateRSAKeyPair() + assert.NoError(t, err) + if FetchSigningKeyPairFromArtifactory(t, artifactoryURL, apiKey) { + return privateKeyFilePath + } + UploadSigningKeyPairToArtifactory(t, artifactoryURL, apiKey, privateKeyFilePath, publicKeyFilePath) + return privateKeyFilePath +} + +func generateRSAKeyPair() (string, string, error) { + privateKeyString := `-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDFgJe3kRIYML2R +Kjjp70XbF+WVsUWdZLN6H3Hzm3FVhVcHcYpLKGxGhbTVN3yAtAA5CLqe4+BXOybM +ACV2NboEV0KhSXcx6MAShyMm/6Ze4POF07yMifewjOstsrxGg4FkL38n3MYQm7y3 +bipFDXg93uGb8zVWG0wcqa5v1u0dD56xoTGSRrEtdjogFtkYVysXcyg7zKzzfQeH +zFwm3jZAG6wDwIlut00vTO62gVopnll+FZnTDSeYZy4nXh4Qo6v1F/gMmV0fIHNh +ZENjf2Y/TYROr0u67qH3XmgZqsi9hTi+OL2H14iRuwm6erKTenH4XhnvsTIOokcg +EdoE9LDFAgMBAAECggEAVEkvNjNOjg1K8UccF9W5sakunOYU5/kgURdXWZe2U8F+ +ZRpS4wVCxAvuoum1k/V9fNmZTxK/3GpNgdT0J9EA7DZTJKLGIAIM6jtKyKtklGwa +8Ttt5WpBztIs0YlMKSmZECjm8puY2WClNoDowCRh8sGJ9bRiyDcJEdhmLauC8JnI +YmJC1c/vFp0FBw/jw5euEPKa559nIFN5Wbwxrl/6A6S7Lp7AuebLlHeLanu7X7e0 +BNhT6sLOhnHjTFomex/z7eg9g5O577OjuYrw1a+81y6CkXTu6a35tnqg9RWtM7JX +WCjo/f/iO/ZE1F/qmu3x97b6Ljuv3yAFNeKfVEQatwKBgQDoH53rrunSCF+dXQTG +MZ9bTcUCw1a8saugC2guJ+xSt8HA0I6PYvqUZWfYgmMm6J8Vu/h1kj7kGIuugX0W +IX9OPIB86mQa/djTfPWaWmnPYwxRQ8DPkzxkdm2qcldY4UwrPo3nsFvGyD6Xfzkp +d7JlDv0cNtcE+rdIHMSTk/blswKBgQDZ0VCOP2sNAZ5uHeS+ksnmxAD00Jt6VukX +Sw9bsBNFeGP2G3m086xhCPMm0PlmuPitRCdzQypJcAJwQTaOFbf4KLBYpEIo2YJb +QXaiQaQZXeWRxzUWysBmsqcSfzAod4BLwimkSGXbHYC9ryanJ7iliNFzyWSpj/sV +ld9y9p1DpwKBgCHL6KxWDUk9Wt6ImpdYxkD+875RPqG+pKRqxMJjoa7xfk5aj0cl +PCK7GQGXCmSx3efGNIi5wFppkHzZ8aJ1QhncCUEmx2h+qUExonjUzS8a1sJGQR53 +64UdER6OA1W3h+WL+BFRxisNIL/iECqPePPp2MRw36Gj92eSeLScCIitAoGAKzyK +YgIirM1CdpdGfbHDlCQaEH6MLkesMyx6Gvgjiymvpf2kNhAcipJtOapHp2VWL4aU +0iNl9HfgdAnt21xiTUc+YgoQ++zZHGYtN14SRdrGpB5H4oNSl9Akq95FX/MAq4ka +HPsmBM2hbYWkBZAz7d/vu60hZysmaw158mcTpocCgYEAkkLv5jtKEHOCJjrdyYl0 +5Bv3Z22NTUdKaFY8wZnqmVBlJVsDG2D6Ypw3NEAQPKY5PJ44XSsM+nPjbBloyLpJ +k4UTtgRSG5/ZgMcDjJIDZIuIivah/g0I+ZkLBmyh8mOdEL/skGvj4iWH0It0V2l5 +IAecx7gdLfPlyBAFZ5Jp9rc= +-----END PRIVATE KEY-----` + publicKeyString := `-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxYCXt5ESGDC9kSo46e9F +2xfllbFFnWSzeh9x85txVYVXB3GKSyhsRoW01Td8gLQAOQi6nuPgVzsmzAAldjW6 +BFdCoUl3MejAEocjJv+mXuDzhdO8jIn3sIzrLbK8RoOBZC9/J9zGEJu8t24qRQ14 +Pd7hm/M1VhtMHKmub9btHQ+esaExkkaxLXY6IBbZGFcrF3MoO8ys830Hh8xcJt42 +QBusA8CJbrdNL0zutoFaKZ5ZfhWZ0w0nmGcuJ14eEKOr9Rf4DJldHyBzYWRDY39m +P02ETq9Luu6h915oGarIvYU4vji9h9eIkbsJunqyk3px+F4Z77EyDqJHIBHaBPSw +xQIDAQAB +-----END PUBLIC KEY-----` + tempDir := os.TempDir() + privateKeyPath := filepath.Join(tempDir, "private.pem") + pubPath := filepath.Join(tempDir, "public.pem") + err := os.WriteFile(privateKeyPath, []byte(privateKeyString), 0600) + if err != nil { + return "", "", err + } + err = os.WriteFile(pubPath, []byte(publicKeyString), 0644) + if err != nil { + return "", "", err + } + return privateKeyPath, pubPath, nil +} + +func FetchSigningKeyPairFromArtifactory(t *testing.T, artifactoryURL, apiKey string) bool { + url := fmt.Sprintf("%sartifactory/api/security/keypair/%s", artifactoryURL, keyPairName) + t.Logf("Fetching key pair from Artifactory: %s", url) + req, err := http.NewRequest(http.MethodGet, url, nil) + assert.NoError(t, err) + if apiKey != "" { + req.Header.Set("Authorization", "Bearer "+apiKey) + } + client := &http.Client{} + resp, err := client.Do(req) + assert.NoError(t, err) + defer func(Body io.ReadCloser) { + err := Body.Close() + assert.NoError(t, err, "Failed to close response body") + }(resp.Body) + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + t.Fatalf("Failed to fetch key pair, status: %s, body: %s", resp.Status, string(body)) + } + bodyBytes, err := io.ReadAll(resp.Body) + assert.NoError(t, err, "failed to read response body") + var keyPair KeyPair + err = json.Unmarshal(bodyBytes, &keyPair) + assert.NoError(t, err) + assert.Equal(t, keyPairName, keyPair.PairName) + t.Logf("Successfully fetched and saved key pair: %s", keyPair.PairName) + if keyPairName == keyPair.PairName { + return true + } + return false +} + +// UploadSigningKeyPairToArtifactory reads private and public key files and uploads them to Artifactory. +func UploadSigningKeyPairToArtifactory(t *testing.T, artifactoryURL, apiKey, privateKeyPath, publicKeyPath string) { + privateKeyBytes, err := os.ReadFile(privateKeyPath) + if err != nil { + t.Fatalf("Failed to read private key file: %v", err) + } + pubKeyBytes, err := os.ReadFile(publicKeyPath) + assert.NoError(t, err) + url := fmt.Sprintf("%sartifactory/api/security/keypair", artifactoryURL) + t.Logf("Keypair create URL %s", url) + reqBody := KeyPair{ + PairName: keyPairName, + PairType: "RSA", + Alias: KeyPairAlias, + PrivateKey: string(privateKeyBytes), + PublicKey: string(pubKeyBytes), + } + jsonBody, err := json.Marshal(reqBody) + assert.NoError(t, err) + req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(jsonBody)) + assert.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + if apiKey != "" { + req.Header.Set("Authorization", "Bearer "+apiKey) + } + client := &http.Client{} + resp, err := client.Do(req) + assert.NoError(t, err) + defer func(Body io.ReadCloser) { + err := Body.Close() + if err != nil { + assert.NoError(t, err, "Failed to close response body") + } + }(resp.Body) + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusNoContent { + body, _ := io.ReadAll(resp.Body) + t.Fatalf("Failed to upload private key, status: %s, body: %s", resp.Status, string(body)) + } +} + +// FetchEvidenceFromArtifactory fetches evidence using GraphQL API +func FetchEvidenceFromArtifactory(t *testing.T, artifactoryURL, apiKey, packageRepo, packageName, packageVersion string) ([]byte, error) { + // Construct the GraphQL API URL + url := fmt.Sprintf("%sonemodel/api/v1/graphql", clientUtils.AddTrailingSlashIfNeeded(artifactoryURL)) + + t.Logf("Fetching evidence from GraphQL API: %s", url) + + // Construct the GraphQL query using the working format + query := fmt.Sprintf(`{ + evidence { + searchEvidence(where:{hasSubjectWith:{repositoryKey:"%s"}}) { + edges { + node { + name + path + repositoryKey + downloadPath + sha256 + predicateType + createdAt + createdBy + verified + predicateSlug + } + } + } + } + }`, packageRepo) + + // Create request payload + requestBody, err := json.Marshal(map[string]string{ + "query": query, + }) + if err != nil { + return nil, fmt.Errorf("failed to create GraphQL request: %v", err) + } + + // Create the request + req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(requestBody)) + if err != nil { + return nil, fmt.Errorf("failed to create request: %v", err) + } + + // Set headers + req.Header.Set("Content-Type", "application/json") + if apiKey != "" { + req.Header.Set("Authorization", "Bearer "+apiKey) + } else { + t.Fatal("API key is required to fetch evidence from Artifactory") + } + // Send the request + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to send request: %v", err) + } + defer func(Body io.ReadCloser) { + err := Body.Close() + if err != nil { + assert.NoError(t, err, "Failed to close response body") + } + }(resp.Body) + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("failed to fetch evidence, status: %s, body: %s", resp.Status, string(body)) + } + // Read the response body + bodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %v", err) + } + return bodyBytes, nil +} diff --git a/testdata/maven/mavenprojectwithsonar/.jfrog/evidence/evidence.yaml b/testdata/maven/mavenprojectwithsonar/.jfrog/evidence/evidence.yaml new file mode 100644 index 000000000..57825208c --- /dev/null +++ b/testdata/maven/mavenprojectwithsonar/.jfrog/evidence/evidence.yaml @@ -0,0 +1,6 @@ +sonar: + url: http://localhost:9000 + reportTaskFile: target/sonar/report-task.txt + maxRetries: 3 + retryIntervalInSecs: 10 + proxy: "" diff --git a/testdata/maven/mavenprojectwithsonar/.jfrog/projects/maven.yaml b/testdata/maven/mavenprojectwithsonar/.jfrog/projects/maven.yaml new file mode 100644 index 000000000..3901f4364 --- /dev/null +++ b/testdata/maven/mavenprojectwithsonar/.jfrog/projects/maven.yaml @@ -0,0 +1,10 @@ +version: 1 +type: maven +resolver: + serverId: artifactory-server + snapshotRepo: dev-maven-virtual + releaseRepo: dev-maven-virtual +deployer: + serverId: artifactory-server + snapshotRepo: dev-maven-local + releaseRepo: dev-maven-local diff --git a/testdata/maven/mavenprojectwithsonar/evidence.yaml b/testdata/maven/mavenprojectwithsonar/evidence.yaml new file mode 100644 index 000000000..86e36a516 --- /dev/null +++ b/testdata/maven/mavenprojectwithsonar/evidence.yaml @@ -0,0 +1,11 @@ +sonar: + url: http://localhost:9000 + reportTaskFile: target/sonar/report-task.txt + maxRetries: 3 + retryIntervalInSecs: 10 + proxy: "" +buildPublish: + enabled: true + evidenceProvider: sonar + keyAlias: test-signing-key + keyPath: /tmp/private.pem diff --git a/testdata/maven/mavenprojectwithsonar/pom.xml b/testdata/maven/mavenprojectwithsonar/pom.xml new file mode 100644 index 000000000..d6a5b0b1d --- /dev/null +++ b/testdata/maven/mavenprojectwithsonar/pom.xml @@ -0,0 +1,52 @@ + + 4.0.0 + + com.example + demo-sonar + 1.0 + demo-sonar + + + 1.8 + 1.8 + UTF-8 + + + + + central + https://repo.maven.apache.org/maven2 + + + + + + + junit + junit + 4.13.2 + test + + + junit + junit + 4.13.1 + test + + + + + + + + org.sonarsource.scanner.maven + sonar-maven-plugin + 3.11.0.3922 + + + + + \ No newline at end of file diff --git a/testdata/maven/mavenprojectwithsonar/src/main/java/com/example/App.java b/testdata/maven/mavenprojectwithsonar/src/main/java/com/example/App.java new file mode 100644 index 000000000..c09c107b1 --- /dev/null +++ b/testdata/maven/mavenprojectwithsonar/src/main/java/com/example/App.java @@ -0,0 +1,12 @@ +package com.example; + +public class App { + public static void main(String[] args) { + System.out.println("Hello from demo-sonar!"); + System.out.println("May the frog be with you!"); + } + + public int add(int a, int b) { + return a + b; + } +} \ No newline at end of file diff --git a/testdata/maven/mavenprojectwithsonar/src/test/java/com/example/AppTest.java b/testdata/maven/mavenprojectwithsonar/src/test/java/com/example/AppTest.java new file mode 100644 index 000000000..3452c6b01 --- /dev/null +++ b/testdata/maven/mavenprojectwithsonar/src/test/java/com/example/AppTest.java @@ -0,0 +1,15 @@ +package com.example; + +import org.junit.Test; + +import static junit.framework.Assert.assertEquals; +import static org.junit.Assert.*; + +public class AppTest { + + @Test + public void testAdd() { + App app = new App(); + assertEquals(5, app.add(2, 3)); + } +} \ No newline at end of file diff --git a/utils/cliutils/commandsflags.go b/utils/cliutils/commandsflags.go index ea6598c43..bcc65ea8a 100644 --- a/utils/cliutils/commandsflags.go +++ b/utils/cliutils/commandsflags.go @@ -790,15 +790,15 @@ var flagsMap = map[string]cli.Flag{ }, uploadMinSplit: cli.StringFlag{ Name: MinSplit, - Usage: "[Default: " + strconv.Itoa(flagkit.UploadMinSplitMb) + "] The minimum file size in MiB required to attempt a multi-part upload. This option, as well as the functionality of multi-part upload, requires Artifactory with S3 or GCP storage.` `", + Usage: "[Default: " + "" + "] The minimum file size in MiB required to attempt a multi-part upload. This option, as well as the functionality of multi-part upload, requires Artifactory with S3 or GCP storage.` `", }, uploadSplitCount: cli.StringFlag{ Name: SplitCount, - Usage: "[Default: " + strconv.Itoa(flagkit.UploadSplitCount) + "] The maximum number of parts that can be concurrently uploaded per file during a multi-part upload. Set to 0 to disable multi-part upload. This option, as well as the functionality of multi-part upload, requires Artifactory with S3 or GCP storage.` `", + Usage: "[Default: " + "" + "] The maximum number of parts that can be concurrently uploaded per file during a multi-part upload. Set to 0 to disable multi-part upload. This option, as well as the functionality of multi-part upload, requires Artifactory with S3 or GCP storage.` `", }, ChunkSize: cli.StringFlag{ Name: ChunkSize, - Usage: "[Default: " + strconv.Itoa(flagkit.UploadChunkSizeMb) + "] The upload chunk size in MiB that can be concurrently uploaded during a multi-part upload. This option, as well as the functionality of multi-part upload, requires Artifactory with S3 or GCP storage.` `", + Usage: "[Default: " + "" + "] The upload chunk size in MiB that can be concurrently uploaded during a multi-part upload. This option, as well as the functionality of multi-part upload, requires Artifactory with S3 or GCP storage.` `", }, syncDeletesQuiet: cli.BoolFlag{ Name: quiet, diff --git a/utils/cliutils/utils.go b/utils/cliutils/utils.go index 4a11ec798..fea7e2672 100644 --- a/utils/cliutils/utils.go +++ b/utils/cliutils/utils.go @@ -4,7 +4,6 @@ import ( "encoding/json" "errors" "fmt" - "github.com/jfrog/jfrog-cli-artifactory/cliutils/flagkit" "io" "net/http" "os" @@ -233,15 +232,15 @@ func CreateSummaryReportString(success, failed int, failNoOp bool, err error) (s func CreateUploadConfiguration(c *cli.Context) (uploadConfiguration *artifactoryUtils.UploadConfiguration, err error) { uploadConfiguration = new(artifactoryUtils.UploadConfiguration) - uploadConfiguration.MinSplitSizeMB, err = getMinSplit(c, flagkit.UploadMinSplitMb) + uploadConfiguration.MinSplitSizeMB, err = getMinSplit(c, 64) if err != nil { return nil, err } - uploadConfiguration.ChunkSizeMB, err = getUploadChunkSize(c, flagkit.UploadChunkSizeMb) + uploadConfiguration.ChunkSizeMB, err = getUploadChunkSize(c, 64) if err != nil { return nil, err } - uploadConfiguration.SplitCount, err = getSplitCount(c, flagkit.UploadSplitCount, flagkit.UploadMaxSplitCount) + uploadConfiguration.SplitCount, err = getSplitCount(c, 64, 64) if err != nil { return nil, err } diff --git a/utils/tests/utils.go b/utils/tests/utils.go index a4b289395..ae81303d0 100644 --- a/utils/tests/utils.go +++ b/utils/tests/utils.go @@ -71,6 +71,7 @@ var ( TestAccess *bool TestTransfer *bool TestLifecycle *bool + TestSonar *bool HideUnitTestLog *bool ciRunId *string InstallDataTransferPlugin *bool @@ -110,6 +111,7 @@ func init() { HideUnitTestLog = flag.Bool("test.hideUnitTestLog", false, "Hide unit tests logs and print it in a file") InstallDataTransferPlugin = flag.Bool("test.installDataTransferPlugin", false, "Install data-transfer plugin on the source Artifactory server") ciRunId = flag.String("ci.runId", "", "A unique identifier used as a suffix to create repositories and builds in the tests") + TestSonar = flag.Bool("test.sonarIntegration", false, "Test Sonar Integration") } func CleanFileSystem() {