-
Notifications
You must be signed in to change notification settings - Fork 0
feat: first implementation #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Signed-off-by: Gustavo Carvalho <gustavo.carvalho@container-solutions.com>
Signed-off-by: Gustavo Carvalho <gustavo.carvalho@container-solutions.com>
Signed-off-by: Gustavo Carvalho <gustavo.carvalho@container-solutions.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR implements the first version of a Jira compliance plugin for the Continuous Compliance Framework. The plugin collects data from Jira Platform, Service Management, and Software APIs to evaluate change request compliance and generate evidence.
Key changes:
- Implements OAuth2 and token-based authentication for Jira Cloud APIs
- Collects comprehensive Jira data including projects, workflows, issues, approvals, SLAs, and deployment information
- Integrates with the compliance-framework agent using the plugin architecture
- Sets up CI/CD workflows for testing and releasing
Reviewed changes
Copilot reviewed 11 out of 13 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
| main.go | Core plugin implementation with configuration handling, data collection orchestration, and policy evaluation logic |
| internal/jira/types.go | Type definitions for Jira API responses including custom time handling for audit records |
| internal/jira/client.go | HTTP client implementation with OAuth2 and token authentication, API endpoint methods for data fetching |
| go.mod | Go module definition with dependencies (contains invalid Go version) |
| go.sum | Dependency checksums for reproducible builds |
| PLAN.md | Development roadmap and architecture documentation |
| Makefile | Build automation with clean and build targets |
| .goreleaser.yaml | Release configuration for multi-platform builds |
| .gitignore | Standard ignore patterns for Go projects |
| .github/workflows/*.yml | CI/CD workflows for testing, building, and releasing the plugin |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| } | ||
|
|
||
| func (c *Client) FetchProjectRemoteLinks(ctx context.Context, projectKey string) ([]JiraRemoteLink, error) { | ||
| // Placeholder as requested in original code |
Copilot
AI
Jan 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The FetchProjectRemoteLinks method is a placeholder that always returns nil without any implementation. If this functionality is not yet needed, it should be documented with a TODO comment explaining why it's not implemented or when it will be implemented.
| // Placeholder as requested in original code | |
| // TODO: Implement fetching project remote links for the given projectKey via the Jira API | |
| // when this functionality is required by the application. |
main.go
Outdated
| indentedJSON, _ := json.MarshalIndent(jiraData, "", " ") | ||
| os.WriteFile("/data/jira_data.json", indentedJSON, 0o644) |
Copilot
AI
Jan 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Writing collected data to a hard-coded file path '/data/jira_data.json' is problematic for several reasons: the path may not exist, it lacks proper error handling, and it's not configurable. Consider making this path configurable or removing this debug code entirely before production use.
main.go
Outdated
| indentedJSON, _ := json.MarshalIndent(jiraData, "", " ") | ||
| os.WriteFile("/data/jira_data.json", indentedJSON, 0o644) |
Copilot
AI
Jan 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error from writing the file is ignored (line 167). While the data is also written successfully before this (line 166), the file write error should be checked and logged at minimum to avoid silent failures during debugging.
|
|
||
| func fetchCloudID(baseURL string) (string, error) { | ||
| // Make request to get tenant info | ||
| resp, err := http.Get(baseURL + "/_edge/tenant_info") |
Copilot
AI
Jan 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The fetchCloudID function uses http.Get without any timeout configured, which could cause the request to hang indefinitely. Consider using a context with timeout or creating an http.Client with appropriate timeout settings.
internal/jira/client.go
Outdated
| // Clone the request and set the bearer token | ||
| newReq := req.Clone(req.Context()) | ||
| newReq.Header.Set("Authorization", "Bearer "+tokenResp.AccessToken) | ||
| t.logger.Debug("Making request with Bearer token", "url", newReq.URL.String(), "auth", "Bearer "+tokenResp.AccessToken[:10]+"...") |
Copilot
AI
Jan 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sensitive information (the first 10 characters of the access token) is being logged. Even partial token exposure in logs can be a security risk and should be avoided. Consider logging only that authentication succeeded without including token data.
| t.logger.Debug("Making request with Bearer token", "url", newReq.URL.String(), "auth", "Bearer "+tokenResp.AccessToken[:10]+"...") | |
| t.logger.Debug("Making request with Bearer token", "url", newReq.URL.String(), "auth", "Bearer <redacted>") |
main.go
Outdated
| return nil | ||
| } | ||
|
|
||
| // TrackedFileInfo holds information about a tracked file and its attestation |
Copilot
AI
Jan 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment says "TrackedFileInfo holds information about a tracked file and its attestation" but the struct is actually JiraPlugin, not TrackedFileInfo. This appears to be copy-pasted documentation that was not updated.
| // TrackedFileInfo holds information about a tracked file and its attestation | |
| // JiraPlugin implements the Jira integration plugin, managing configuration and the Jira HTTP client. |
internal/jira/client.go
Outdated
| url := fmt.Sprintf("/rest/api/3/workflowscheme/project?%s", params.Encode()) | ||
| resp, err := c.do(ctx, "GET", url, nil) |
Copilot
AI
Jan 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The variable name 'url' shadows the imported 'url' package (imported on line 9), which can lead to confusion and potential bugs when trying to use url package functions later in this function.
| url := fmt.Sprintf("/rest/api/3/workflowscheme/project?%s", params.Encode()) | |
| resp, err := c.do(ctx, "GET", url, nil) | |
| endpoint := fmt.Sprintf("/rest/api/3/workflowscheme/project?%s", params.Encode()) | |
| resp, err := c.do(ctx, "GET", endpoint, nil) |
| // Read the raw response for debugging | ||
| body, err := io.ReadAll(resp.Body) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| c.Logger.Info("Raw audit records response", "body", string(body)) | ||
|
|
||
| var result struct { | ||
| Records []JiraAuditRecord `json:"records"` | ||
| } | ||
| if err := json.Unmarshal(body, &result); err != nil { | ||
| return nil, err | ||
| } |
Copilot
AI
Jan 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The response body is read twice: once for debugging output (line 282) and then again for unmarshaling (line 291). After reading the body the first time, it's consumed and cannot be read again. This will cause json.Unmarshal to fail. The body should only be read once, or the bytes should be reused.
| func (t *oauth2Transport) RoundTrip(req *http.Request) (*http.Response, error) { | ||
| // Get OAuth2 token with required scopes | ||
| tokenReq, err := http.NewRequest("POST", t.tokenURL, nil) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| tokenReq.SetBasicAuth(t.clientID, t.secret) | ||
| tokenReq.Header.Set("Content-Type", "application/x-www-form-urlencoded") | ||
|
|
||
| // Form encode the parameters | ||
| data := url.Values{} | ||
| data.Set("grant_type", "client_credentials") | ||
| data.Set("scope", "read:jira-user read:jira-work read:workflow:jira read:permission:jira read:workflow-scheme:jira read:audit-log:jira read:avatar:jira read:group:jira read:issue-type:jira read:project-category:jira read:project:jira read:user:jira read:application-role:jira") | ||
| tokenReq.Body = io.NopCloser(strings.NewReader(data.Encode())) | ||
|
|
||
| resp, err := t.base.RoundTrip(tokenReq) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| defer resp.Body.Close() | ||
|
|
||
| if resp.StatusCode != http.StatusOK { | ||
| body, _ := io.ReadAll(resp.Body) | ||
| return nil, fmt.Errorf("failed to get token: %d, body: %s", resp.StatusCode, string(body)) | ||
| } | ||
|
|
||
| var tokenResp struct { | ||
| AccessToken string `json:"access_token"` | ||
| Scope string `json:"scope"` | ||
| ExpiresIn int `json:"expires_in"` | ||
| TokenType string `json:"token_type"` | ||
| } | ||
| body, _ := io.ReadAll(resp.Body) | ||
| if err := json.Unmarshal(body, &tokenResp); err != nil { | ||
| return nil, fmt.Errorf("failed to decode token response: %w, body: %s", err, string(body)) | ||
| } | ||
| t.logger.Debug("Got OAuth2 token", "scope", tokenResp.Scope, "expiresIn", tokenResp.ExpiresIn) | ||
|
|
||
| // Clone the request and set the bearer token | ||
| newReq := req.Clone(req.Context()) | ||
| newReq.Header.Set("Authorization", "Bearer "+tokenResp.AccessToken) | ||
| t.logger.Debug("Making request with Bearer token", "url", newReq.URL.String(), "auth", "Bearer "+tokenResp.AccessToken[:10]+"...") | ||
| return t.base.RoundTrip(newReq) |
Copilot
AI
Jan 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The OAuth2 token is fetched on every request in the RoundTrip method, which is inefficient. The token should be cached and only refreshed when it expires. This will cause significant performance issues with multiple API calls.
internal/jira/client.go
Outdated
| req.SetBasicAuth(t.email, t.token) | ||
| return t.base.RoundTrip(req) |
Copilot
AI
Jan 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The tokenAuthTransport.RoundTrip method modifies the original request object directly with SetBasicAuth, which could cause issues if the request is reused. The request should be cloned before modification, similar to how it's done in oauth2Transport.
| req.SetBasicAuth(t.email, t.token) | |
| return t.base.RoundTrip(req) | |
| newReq := req.Clone(req.Context()) | |
| newReq.SetBasicAuth(t.email, t.token) | |
| return t.base.RoundTrip(newReq) |
Signed-off-by: Gustavo Carvalho <gustavo.carvalho@container-solutions.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 12 out of 14 changed files in this pull request and generated 14 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
main.go
Outdated
| jiraJSON, _ := json.MarshalIndent(jiraData, "", " ") | ||
| converted := jiraData.ToProjectCentric() | ||
| indentedJSON, _ := json.MarshalIndent(converted, "", " ") |
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Error return values from json.MarshalIndent are silently ignored. These marshaling operations could fail, and the errors should be handled appropriately or at least logged.
| func (t *oauth2Transport) RoundTrip(req *http.Request) (*http.Response, error) { | ||
| // Get OAuth2 token with required scopes | ||
| tokenReq, err := http.NewRequest("POST", t.tokenURL, nil) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| tokenReq.SetBasicAuth(t.clientID, t.secret) | ||
| tokenReq.Header.Set("Content-Type", "application/x-www-form-urlencoded") | ||
|
|
||
| // Form encode the parameters | ||
| data := url.Values{} | ||
| data.Set("grant_type", "client_credentials") | ||
| data.Set("scope", "read:jira-user read:jira-work read:workflow:jira read:permission:jira read:workflow-scheme:jira read:audit-log:jira read:avatar:jira read:group:jira read:issue-type:jira read:project-category:jira read:project:jira read:user:jira read:application-role:jira read:servicedesk-request") | ||
| tokenReq.Body = io.NopCloser(strings.NewReader(data.Encode())) | ||
|
|
||
| resp, err := t.base.RoundTrip(tokenReq) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| defer resp.Body.Close() | ||
|
|
||
| if resp.StatusCode != http.StatusOK { | ||
| body, _ := io.ReadAll(resp.Body) | ||
| return nil, fmt.Errorf("failed to get token: %d, body: %s", resp.StatusCode, string(body)) | ||
| } | ||
|
|
||
| var tokenResp struct { | ||
| AccessToken string `json:"access_token"` | ||
| Scope string `json:"scope"` | ||
| ExpiresIn int `json:"expires_in"` | ||
| TokenType string `json:"token_type"` | ||
| } | ||
| body, _ := io.ReadAll(resp.Body) | ||
| if err := json.Unmarshal(body, &tokenResp); err != nil { | ||
| return nil, fmt.Errorf("failed to decode token response: %w, body: %s", err, string(body)) | ||
| } | ||
| t.logger.Debug("Got OAuth2 token", "scope", tokenResp.Scope, "expiresIn", tokenResp.ExpiresIn) | ||
|
|
||
| // Clone the request and set the bearer token | ||
| newReq := req.Clone(req.Context()) | ||
| newReq.Header.Set("Authorization", "Bearer "+tokenResp.AccessToken) | ||
| t.logger.Debug("Making request with Bearer token", "url", newReq.URL.String(), "auth", "Bearer "+tokenResp.AccessToken[:10]+"...") | ||
| return t.base.RoundTrip(newReq) | ||
| } |
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The OAuth2 token is fetched on every request in the RoundTrip method. This is inefficient and could cause rate limiting issues. Implement token caching with expiration tracking to reuse tokens until they expire.
internal/jira/client.go
Outdated
| c.Logger.Error("Error fetching SLAs", "issue", issueKey, "error", err) | ||
| return nil, err | ||
| } | ||
| defer resp.Body.Close() |
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Multiple deferred resp.Body.Close() calls in a loop can lead to resource leaks because deferred functions in loops don't execute until the function returns. The response bodies should be closed immediately after processing each response, not deferred.
internal/jira/client.go
Outdated
| if err != nil { | ||
| return nil, err | ||
| } | ||
| defer resp.Body.Close() |
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Multiple deferred resp.Body.Close() calls in a loop can lead to resource leaks because deferred functions in loops don't execute until the function returns. The response bodies should be closed immediately after processing each response, not deferred.
internal/jira/client.go
Outdated
| defer resp.Body.Close() | ||
| c.logSuccessOrWarn("FetchProjects", resp, err) | ||
| if resp.StatusCode != http.StatusOK { | ||
| body, _ := io.ReadAll(resp.Body) | ||
| return nil, fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body)) | ||
| } | ||
| body := resp.Body | ||
| var searchResp JiraProjectSearchResponse | ||
| if err := json.NewDecoder(body).Decode(&searchResp); err != nil { | ||
| return nil, err | ||
| } |
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Multiple deferred resp.Body.Close() calls in a loop can lead to resource leaks because deferred functions in loops don't execute until the function returns. The response bodies should be closed immediately after processing each response, not deferred.
| defer resp.Body.Close() | |
| c.logSuccessOrWarn("FetchProjects", resp, err) | |
| if resp.StatusCode != http.StatusOK { | |
| body, _ := io.ReadAll(resp.Body) | |
| return nil, fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body)) | |
| } | |
| body := resp.Body | |
| var searchResp JiraProjectSearchResponse | |
| if err := json.NewDecoder(body).Decode(&searchResp); err != nil { | |
| return nil, err | |
| } | |
| c.logSuccessOrWarn("FetchProjects", resp, err) | |
| if resp.StatusCode != http.StatusOK { | |
| body, _ := io.ReadAll(resp.Body) | |
| resp.Body.Close() | |
| return nil, fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body)) | |
| } | |
| var searchResp JiraProjectSearchResponse | |
| if err := json.NewDecoder(resp.Body).Decode(&searchResp); err != nil { | |
| resp.Body.Close() | |
| return nil, err | |
| } | |
| resp.Body.Close() |
internal/jira/client.go
Outdated
| defer resp.Body.Close() | ||
| c.logSuccessOrWarn("FetchWorkflows", resp, err) | ||
| if resp.StatusCode != http.StatusOK { | ||
| body, _ := io.ReadAll(resp.Body) | ||
| return nil, fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body)) | ||
| } | ||
|
|
||
| var searchResp JiraWorkflowSearchResponse | ||
| if err := json.NewDecoder(resp.Body).Decode(&searchResp); err != nil { | ||
| return nil, err | ||
| } |
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Multiple deferred resp.Body.Close() calls in a loop can lead to resource leaks because deferred functions in loops don't execute until the function returns. The response bodies should be closed immediately after processing each response, not deferred.
| defer resp.Body.Close() | |
| c.logSuccessOrWarn("FetchWorkflows", resp, err) | |
| if resp.StatusCode != http.StatusOK { | |
| body, _ := io.ReadAll(resp.Body) | |
| return nil, fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body)) | |
| } | |
| var searchResp JiraWorkflowSearchResponse | |
| if err := json.NewDecoder(resp.Body).Decode(&searchResp); err != nil { | |
| return nil, err | |
| } | |
| c.logSuccessOrWarn("FetchWorkflows", resp, err) | |
| if resp.StatusCode != http.StatusOK { | |
| body, _ := io.ReadAll(resp.Body) | |
| resp.Body.Close() | |
| return nil, fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body)) | |
| } | |
| var searchResp JiraWorkflowSearchResponse | |
| if err := json.NewDecoder(resp.Body).Decode(&searchResp); err != nil { | |
| resp.Body.Close() | |
| return nil, err | |
| } | |
| resp.Body.Close() |
internal/jira/client.go
Outdated
| if err != nil { | ||
| return nil, err | ||
| } | ||
| defer resp.Body.Close() |
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Multiple deferred resp.Body.Close() calls in a loop can lead to resource leaks because deferred functions in loops don't execute until the function returns. The response bodies should be closed immediately after processing each response, not deferred.
internal/jira/client.go
Outdated
| if err != nil { | ||
| return nil, err | ||
| } | ||
| defer resp.Body.Close() |
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Multiple deferred resp.Body.Close() calls in a loop can lead to resource leaks because deferred functions in loops don't execute until the function returns. The response bodies should be closed immediately after processing each response, not deferred.
internal/jira/client.go
Outdated
| defer resp.Body.Close() | ||
| c.logSuccessOrWarn("GetAllStatuses", resp, err) | ||
|
|
||
| if resp.StatusCode != http.StatusOK { | ||
| body, _ := io.ReadAll(resp.Body) | ||
| return nil, fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body)) | ||
| } | ||
|
|
||
| var searchResp JiraStatusSearchResponse | ||
| if err := json.NewDecoder(resp.Body).Decode(&searchResp); err != nil { | ||
| return nil, err | ||
| } |
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Multiple deferred resp.Body.Close() calls in a loop can lead to resource leaks because deferred functions in loops don't execute until the function returns. The response bodies should be closed immediately after processing each response, not deferred.
| defer resp.Body.Close() | |
| c.logSuccessOrWarn("GetAllStatuses", resp, err) | |
| if resp.StatusCode != http.StatusOK { | |
| body, _ := io.ReadAll(resp.Body) | |
| return nil, fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body)) | |
| } | |
| var searchResp JiraStatusSearchResponse | |
| if err := json.NewDecoder(resp.Body).Decode(&searchResp); err != nil { | |
| return nil, err | |
| } | |
| c.logSuccessOrWarn("GetAllStatuses", resp, err) | |
| if resp.StatusCode != http.StatusOK { | |
| body, _ := io.ReadAll(resp.Body) | |
| resp.Body.Close() | |
| return nil, fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body)) | |
| } | |
| var searchResp JiraStatusSearchResponse | |
| if err := json.NewDecoder(resp.Body).Decode(&searchResp); err != nil { | |
| resp.Body.Close() | |
| return nil, err | |
| } | |
| resp.Body.Close() |
Signed-off-by: Gustavo Carvalho <gustavo.carvalho@container-solutions.com>
Signed-off-by: Gustavo Carvalho <gustavo.carvalho@container-solutions.com>
No description provided.