Skip to content
Closed
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
23 changes: 22 additions & 1 deletion cmd/kosli/attestSonar.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,6 +25,7 @@ type attestSonarOptions struct {
serverURL string
revision string
pullRequest string
branch string
maxWait int
payload SonarAttestationPayload
}
Expand All@@ -48,6 +49,8 @@ exponential backoff between retries. Once the results are available they are att
2. Providing the Sonar project key and either the revision or the pull-request ID of the scan (plus the SonarQube server URL if relevant).
For branch scans: if running the Kosli CLI in some CI/CD pipeline, the revision is defaulted to the commit SHA. If you are running the command locally,
or have overriden the revision in SonarQube via parameters to the Sonar scanner, you can provide the correct revision using the ^--sonar-revision^ flag.
If the scan ran on a branch other than the project's main branch in SonarQube, you must also provide the branch name using the ^--sonar-branch^ flag:
SonarQube only searches the main branch unless it is told otherwise, so without it the scan cannot be found.
For pull request scans: provide the pull-request ID using the ^--pull-request^ flag instead of the revision.
Kosli then finds the scan results for the specified project key and revision or pull-request ID.

Expand DownExpand Up@@ -104,6 +107,18 @@ kosli attest sonar \
--api-token yourAPIToken \
--org yourOrgName \

# report a SonarQube Cloud attestation about a trail using key/revision for a scan on a non-main branch:
kosli attest sonar \
--name yourAttestationName \
--flow yourFlowName \
--trail yourTrailName \
--sonar-api-token yourSonarAPIToken \
--sonar-project-key yourSonarProjectKey \
--sonar-revision yourSonarRevision \
--sonar-branch yourSonarBranchName \
--api-token yourAPIToken \
--org yourOrgName \

# report a SonarQube Cloud attestation about a trail for a pull request scan using key/pull-request:
kosli attest sonar \
--name yourAttestationName \
Expand DownExpand Up@@ -176,6 +191,11 @@ func newAttestSonarCmd(out io.Writer) *cobra.Command {
return err
}

err = MuXRequiredFlags(cmd, []string{"sonar-branch", "pull-request"}, false)
if err != nil {
return err
}

err = ValidateAttestationArtifactArg(args, o.fingerprintOptions.artifactType, o.payload.ArtifactFingerprint)
if err != nil {
return ErrorBeforePrintingUsage(cmd, err.Error())
Expand All@@ -199,6 +219,7 @@ func newAttestSonarCmd(out io.Writer) *cobra.Command {
cmd.Flags().StringVar(&o.serverURL, "sonar-server-url", "https://sonarcloud.io", sonarServerURLFlag)
cmd.Flags().StringVar(&o.revision, "sonar-revision", o.commitSHA, sonarRevisionFlag)
cmd.Flags().StringVar(&o.pullRequest, "pull-request", "", sonarPRFlag)
cmd.Flags().StringVar(&o.branch, "sonar-branch", "", sonarBranchFlag)
Comment thread
AlexKantor87 marked this conversation as resolved.
cmd.Flags().StringVar(&o.ceTaskURL, "sonar-ce-task-url", "", sonarCETaskURLFlag)
cmd.Flags().IntVar(&o.maxWait, "max-wait", 30, sonarMaxWaitFlag)

Expand All@@ -221,7 +242,7 @@ func (o *attestSonarOptions) run(args []string) error {
return err
}

sc := sonar.NewSonarConfig(o.apiToken, o.workingDir, o.ceTaskURL, o.projectKey, o.serverURL, o.revision, o.pullRequest, o.maxWait)
sc := sonar.NewSonarConfig(o.apiToken, o.workingDir, o.ceTaskURL, o.projectKey, o.serverURL, o.revision, o.pullRequest, o.branch, o.maxWait)

o.payload.SonarResults, err = sc.GetSonarResults(logger)
if err != nil {
Expand Down
10 changes: 8 additions & 2 deletions cmd/kosli/attestSonar_test.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -182,7 +182,7 @@ func (suite *AttestSonarCommandTestSuite) TestAttestSonarCmd() {
wantError: true,
name: "16 if incorrect revision given (or the scan for the given revision has been deleted by SonarCloud)",
cmd: fmt.Sprintf("attest sonar --name cli.foo --commit HEAD --origin-url http://www.example.com --sonar-project-key cyber-dojo_differ --sonar-revision b4d1053f2aac18c9fb4b9a289a8289199c932e12 %s", suite.defaultKosliArguments),
golden: "Error: analysis for revision b4d1053f2aac18c9fb4b9a289a8289199c932e12 of project cyber-dojo_differ not found. Check the revision is correct. \nThe scan may still be being processed by SonarQube, try again later.\n Otherwise if you are attesting an older scan, the snapshot may also have been deleted by SonarQube\n",
golden: "Error: analysis for revision b4d1053f2aac18c9fb4b9a289a8289199c932e12 of project cyber-dojo_differ not found: only the project's main branch was searched, because no --sonar-branch was given. Check the revision is correct, and pass --sonar-branch if the scan ran on another branch. \nThe scan may still be being processed by SonarQube, try again later.\n Otherwise if you are attesting an older scan, the snapshot may also have been deleted by SonarQube\n",
},
{
wantError: true,
Expand DownExpand Up@@ -257,6 +257,12 @@ func (suite *AttestSonarCommandTestSuite) TestAttestSonarCmd() {
cmd: fmt.Sprintf("attest sonar --name .foo %s", suite.defaultKosliArguments),
golden: "Error: failed to parse attestation name: invalid attestation name format: .foo\n",
},
{
wantError: true,
name: "30 can't provide both sonar-branch and pull-request",
cmd: fmt.Sprintf("attest sonar --name cli.foo --commit HEAD --origin-url http://www.example.com --sonar-project-key cyber-dojo_differ --sonar-branch release/uat --pull-request 5 %s", suite.defaultKosliArguments),
golden: "Error: only one of --sonar-branch, --pull-request is allowed\n",
},
}

runTestCmd(suite.T(), tests)
Expand DownExpand Up@@ -339,7 +345,7 @@ func (suite *AttestSonarQubeCommandTestSuite) TestAttestSonarQubeCmd() {
wantError: true,
name: "113 if incorrect revision given, give an error",
cmd: fmt.Sprintf("attest sonar --name cli.foo --commit HEAD --origin-url http://www.example.com --sonar-server-url http://localhost:9000 --sonar-project-key test5 --sonar-revision 8e6f9489e5f2ddf8e719b503e374975e8b607fd2 %s", suite.defaultKosliArguments),
golden: "Error: analysis for revision 8e6f9489e5f2ddf8e719b503e374975e8b607fd2 of project test5 not found. Check the revision is correct. Snapshot may also have been deleted by SonarQube\n",
golden: "Error: analysis for revision 8e6f9489e5f2ddf8e719b503e374975e8b607fd2 of project test5 not found: only the project's main branch was searched, because no --sonar-branch was given. Check the revision is correct, and pass --sonar-branch if the scan ran on another branch. \nThe scan may still be being processed by SonarQube, try again later.\n Otherwise if you are attesting an older scan, the snapshot may also have been deleted by SonarQube\n",
},
{
wantError: true,
Expand Down
3 changes: 2 additions & 1 deletion cmd/kosli/root.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -301,7 +301,8 @@ The ^.kosli_ignore^ will be treated as part of the artifact like any other file,
sonarProjectKeyFlag = "[conditional] The project key of the SonarQube project. Only required if you want to use the project key/revision/pull-request to get the scan results rather than using Sonar's metadata file."
sonarServerURLFlag = "[conditional] The URL of your SonarQube server. Only required if you are using SonarQube Server and not using SonarQube's metadata file to get scan results."
sonarRevisionFlag = "[conditional] The revision of the SonarQube project. Only required if you want to use the project key/revision to get the scan results rather than using Sonar's metadata file and you have overridden the default revision, or you aren't using a CI. Defaults to the value of the git commit flag. Cannot be used with --pull-request."
sonarPRFlag = "[conditional] The ID of the pull-request. Only required if you want to use the project key/pull-request to get the scan results rather than using Sonar's metadata file. Cannot be used with --sonar-revision."
sonarPRFlag = "[conditional] The ID of the pull-request. Only required if you want to use the project key/pull-request to get the scan results rather than using Sonar's metadata file. Cannot be used with --sonar-revision or --sonar-branch."
sonarBranchFlag = "[conditional] The name of the branch the SonarQube scan ran on. Only required if you are using the project key/revision to get the scan results and the scan ran on a branch other than the project's main branch in SonarQube. Cannot be used with --pull-request."
Comment thread
AlexKantor87 marked this conversation as resolved.
sonarMaxWaitFlag = "[optional] Allow the command to wait and retry fetching the scan results from SonarQube, up to the maximum number of seconds provided, with exponential backoff. Useful when using SonarQube's metadata file to retrieve and attest scans that take a long time to process . Defaults to 30 seconds."
sonarCETaskURLFlag = "[conditional] The URL of the SonarQube CE task. Can be used instead of --sonar-working-dir when the report-task.txt file is not accessible, e.g. due to container isolation in CI/CD pipelines."
logicalEnvFlag = "[required] The logical environment."
Expand Down
1 change: 1 addition & 0 deletions cmd/kosli/testdata/empty-flag-audit-coverage.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -459,6 +459,7 @@
"repo-url": "string",
"repository": "string",
"sonar-api-token": "string",
"sonar-branch": "string",
"sonar-ce-task-url": "string",
"sonar-project-key": "string",
"sonar-revision": "string",
Expand Down
1 change: 1 addition & 0 deletions hack/empty-flag-audit/categories.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -136,6 +136,7 @@
"show-input": "output",
"show-unchanged": "output",
"sonar-api-token": "credentials",
"sonar-branch": "identity",
"sonar-ce-task-url": "location",
"sonar-project-key": "identity",
"sonar-revision": "identity",
Expand Down
2 changes: 2 additions & 0 deletions hack/empty-flag-audit/spec.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -1177,6 +1177,7 @@
"repo-url",
"repository",
"sonar-api-token",
"sonar-branch",
Comment thread
AlexKantor87 marked this conversation as resolved.
"sonar-ce-task-url",
"sonar-project-key",
"sonar-revision",
Expand DownExpand Up@@ -1211,6 +1212,7 @@
"repo-url": "http://example.com",
"repository": "probe-repository",
"sonar-api-token": "probe-sonar-api-token",
"sonar-branch": "probe-sonar-branch",
"sonar-ce-task-url": "probe-sonar-ce-task-url",
"sonar-project-key": "probe-sonar-project-key",
"sonar-revision": "probe-sonar-revision",
Expand Down
72 changes: 63 additions & 9 deletions internal/sonar/sonar.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -22,6 +22,7 @@ type SonarConfig struct {
projectKey string
serverURL string
pullRequest string
branch string
maxWait int
}

Expand DownExpand Up@@ -135,7 +136,7 @@ type Error struct {
Msg string `json:"msg"`
}

func NewSonarConfig(apiToken, workingDir, ceTaskUrl, projectKey, serverURL, revision, pullRequest string, maxWait int) *SonarConfig {
func NewSonarConfig(apiToken, workingDir, ceTaskUrl, projectKey, serverURL, revision, pullRequest, branch string, maxWait int) *SonarConfig {
return &SonarConfig{
APIToken: apiToken,
WorkingDir: workingDir,
Expand All@@ -144,6 +145,7 @@ func NewSonarConfig(apiToken, workingDir, ceTaskUrl, projectKey, serverURL, revi
projectKey: projectKey,
serverURL: serverURL,
pullRequest: pullRequest,
branch: branch,
maxWait: maxWait,
}
}
Expand All@@ -158,6 +160,19 @@ func sonarURL(serverURL, apiPath string, params url.Values) (string, error) {
return u.String(), nil
}

// analysesSearchURL builds a project_analyses/search URL, scoped to the branch
// when there is one: SonarQube otherwise searches only the project's main branch,
// so an analysis on any other branch is invisible (#861, #1116). Both lookups
// share this so the rule cannot be fixed on one path and left broken on the other,
// which is how those two issues came to be six months apart.
func analysesSearchURL(sonarResults *SonarResults, project *Project) (string, error) {
params := url.Values{"project": {project.Key}}
if sonarResults.Branch != nil && sonarResults.Branch.Name != "" {
params.Set("branch", sonarResults.Branch.Name)
}
return sonarURL(sonarResults.ServerUrl, "api/project_analyses/search", params)
}

func (sc *SonarConfig) GetSonarResults(logger *log.Logger) (*SonarResults, error) {
var analysisID string
var err error
Expand DownExpand Up@@ -197,6 +212,13 @@ func (sc *SonarConfig) GetSonarResults(logger *log.Logger) (*SonarResults, error
project.Key = sc.projectKey
sonarResults.ServerUrl = sc.serverURL
sonarResults.Revision = sc.revision
// On this path there is no CE task to read the branch from, so the branch
// can only come from the user (#1116). Set it on the results, which is the
// one mechanism both analyses lookups use to scope their search. A pull
// request scan is not a branch scan, so the branch is not carried there.
if sc.branch != "" && sonarResults.PullRequest == "" {
sonarResults.Branch = &Branch{Name: sc.branch}
}
Comment thread
AlexKantor87 marked this conversation as resolved.
Comment thread
AlexKantor87 marked this conversation as resolved.
project.Url, err = sonarURL(sonarResults.ServerUrl, "dashboard", url.Values{"id": {project.Key}})
if err != nil {
return nil, err
Expand All@@ -212,10 +234,39 @@ func (sc *SonarConfig) GetSonarResults(logger *log.Logger) (*SonarResults, error
if err != nil {
return nil, err
}
// The task's own branch wins when it reports one — it is the scan's own
// record, reached via an analysis ID the branch-scoped search returned.
// But it must not delete the branch the user gave us when it reports none,
// which SonarQube does for main-branch tasks and older self-hosted Servers
// do more widely (#1116).
//
// Unlike the guard above, PullRequest here can also have been set from the
// matched task, so this must read the results field rather than the config.
if sc.branch != "" && sonarResults.PullRequest == "" &&
(sonarResults.Branch == nil || sonarResults.Branch.Name == "") {
sonarResults.Branch = &Branch{Name: sc.branch}
}
}
}

if analysisID == "" && sc.CETaskUrl != "" {
// Here the scan is identified by report-task.txt or --sonar-ce-task-url, and
// the branch is read from the scan task, so a supplied branch reaches nothing.
// Say so rather than ignoring it in silence.
//
// Reaching this block is what makes that true: readFile puts the file's
// ceTaskUrl on sc.CETaskUrl, so both ways of naming a scan by its task arrive
// here, and an empty analysisID means the project-key path did not already
// resolve one.
//
// Unlike the missing-task warning at the end of this function, this one is
// deliberately emitted before the lookups that can fail. "The flag you passed
// is ignored" is true whether or not the run then succeeds, and is worth
// saying either way; the other warning describes a payload, so it would be a
// false statement on a run that never publishes one.
Comment on lines +262 to +266

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale after bbfc33fb: there is no longer a missing-task warning at the end of this function, so this paragraph contrasts against something a reader can't find. It's the only comment in the file that still refers to the reverted warning (grep "missing-task" returns just this line).

The rule it states is worth keeping — it's the reasoning that decides where this warning goes — it just needs to stand on its own rather than lean on the removed one:

Suggested change
// Unlike the missing-task warning at the end of this function, this one is
// deliberately emitted before the lookups that can fail. "The flag you passed
// is ignored" is true whether or not the run then succeeds, and is worth
// saying either way; the other warning describes a payload, so it would be a
// false statement on a run that never publishes one.
// Deliberately emitted before the lookups that can fail: "the flag you passed
// is ignored" is true whether or not the run then succeeds, and a user whose
// run fails for an unrelated reason still wants to know their branch flag
// reached nothing. Anything that instead describes the payload must wait until
// there is a payload to describe.

if sc.branch != "" {
logger.Warn("--sonar-branch is ignored when the scan is identified by report-task.txt or --sonar-ce-task-url: the branch is read from the scan task")
}
Comment thread
AlexKantor87 marked this conversation as resolved.
//Get the analysis ID, status, project name and branch data from the ceTaskURL (ce API)
analysisID, err = GetCETaskData(httpClient, project, sonarResults, sc.CETaskUrl, sc.maxWait, logger)
if err != nil {
Expand DownExpand Up@@ -380,7 +431,7 @@ func GetCETaskData(httpClient *http.Client, project *Project, sonarResults *Sona
func GetProjectAnalysisFromRevision(httpClient *http.Client, sonarResults *SonarResults, project *Project, revision string, logger *log.Logger) (string, error) {
var analysisID string

projectAnalysesURL, err := sonarURL(sonarResults.ServerUrl, "api/project_analyses/search", url.Values{"project": {project.Key}})
projectAnalysesURL, err := analysesSearchURL(sonarResults, project)
if err != nil {
return "", err
}
Expand DownExpand Up@@ -418,19 +469,22 @@ func GetProjectAnalysisFromRevision(httpClient *http.Client, sonarResults *Sonar
}

if sonarResults.AnalysedAt == "" {
return "", fmt.Errorf("analysis for revision %s of project %s not found. Check the revision is correct. \nThe scan may still be being processed by SonarQube, try again later.\n Otherwise if you are attesting an older scan, the snapshot may also have been deleted by SonarQube", revision, project.Key)
// An empty result reads like a permissions problem, so say which branch was
// actually searched: unscoped, SonarQube only searches the main branch (#1116).
scope := "only the project's main branch was searched, because no --sonar-branch was given"
advice := "Check the revision is correct, and pass --sonar-branch if the scan ran on another branch."
if sonarResults.Branch != nil && sonarResults.Branch.Name != "" {
scope = fmt.Sprintf("branch %s was searched", sonarResults.Branch.Name)
advice = "Check the revision and the branch are correct."
}
return "", fmt.Errorf("analysis for revision %s of project %s not found: %s. %s \nThe scan may still be being processed by SonarQube, try again later.\n Otherwise if you are attesting an older scan, the snapshot may also have been deleted by SonarQube", revision, project.Key, scope, advice)
}

return analysisID, nil
}

func GetProjectAnalysisFromAnalysisID(httpClient *http.Client, sonarResults *SonarResults, project *Project, analysisID string) error {
// Forward branch to find analyses on non-default branches (#861).
params := url.Values{"project": {project.Key}}
if sonarResults.Branch != nil && sonarResults.Branch.Name != "" {
params.Set("branch", sonarResults.Branch.Name)
}
projectAnalysesURL, err := sonarURL(sonarResults.ServerUrl, "api/project_analyses/search", params)
projectAnalysesURL, err := analysesSearchURL(sonarResults, project)
if err != nil {
return err
}
Expand Down
Loading
Loading