Skip to content
Merged
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
4 changes: 2 additions & 2 deletions .github/workflows/lint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,6 @@ jobs:
- name: Run golangci-lint
uses: golangci/golangci-lint-action@aaa42aa0628b4ae2578232a66b541047968fac86 # v6.1.0
with:
version: latest
skip-go-installation: true
version: v2.10.1
args: --timeout=5m
only-new-issues: true
42 changes: 13 additions & 29 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
version: "2"

linters-settings:
dupl:
threshold: 100
funlen:
lines: 150
statements: 50
statements: 80
goconst:
min-len: 2
min-occurrences: 2
Expand All @@ -22,16 +24,17 @@ linters-settings:
- wrapperFunc
gocyclo:
min-complexity: 15
goimports:
local-prefixes: github.com/golangci/golangci-lint
golint:
min-confidence: 0
gomnd:
settings:
mnd:
checks: argument,case,condition,return
revive:
confidence: 0
mnd:
checks:
- argument
- case
- condition
- return
govet:
shadow: true
enable:
- shadow
settings:
printf:
funcs:
Expand All @@ -41,8 +44,6 @@ linters-settings:
- (github.com/golangci/golangci-lint/pkg/logutils.Log).Fatalf
lll:
line-length: 140
maligned:
suggest-new: true
misspell:
locale: US

Expand All @@ -54,35 +55,22 @@ linters:
- dupl
- errcheck
- copyloopvar
- funlen
- gochecknoinits
- goconst
- gocritic
- gocyclo
- gofmt
- goimports
- goprintffuncname
- gosec
- gosimple
- govet
- ineffassign
- lll
- misspell
- nakedret
- revive
- rowserrcheck
- staticcheck
- stylecheck
- unconvert
- unparam
- unused
- whitespace

don't enable:
- typecheck
- typechecker
- depguard

issues:
exclude-dirs:
- test/testdata_etc
Expand All @@ -94,7 +82,3 @@ issues:
linters:
- revive

service:
golangci-lint-version: 1.63.4 # Updated to match latest stable version
prepare:
- echo "Custom preparation commands go here"
7 changes: 4 additions & 3 deletions internal/app/export/transform.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,19 +76,20 @@ func TransformXMLInstallationMappings(installationMappings *soap.GetInstallation
}

for _, e := range installationMappings.GetInstallationSettingsResult.InstallationSettingsList.InstallationSetting {
if e.Name == InstallationEngineServiceName {
switch e.Name {
case InstallationEngineServiceName:
out = append(out, &common.InstallationMapping{
Name: InstallationEngineServiceName,
Version: e.Version,
Hotfix: e.Hotfix,
})
} else if e.Name == installationScansManagerName {
case installationScansManagerName:
scansManager = &common.InstallationMapping{
Name: InstallationEngineServiceName,
Version: e.Version,
Hotfix: e.Hotfix,
}
} else if e.Name == installationContentPackName {
case installationContentPackName:
out = append(out, &common.InstallationMapping{
Name: e.Name,
Version: e.Version,
Expand Down
7 changes: 4 additions & 3 deletions internal/app/permissions/permissions.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,12 @@ func GetFromExportOptions(exportOptions []string) []interface{} {
resultsPermissions := []string{useOdataPermission, generateScanReportPermission, viewResults}

for _, exportOption := range exportOptions {
if exportOption == export.UsersOption {
switch exportOption {
case export.UsersOption:
output = append(output, usersPermissions...)
} else if exportOption == export.TeamsOption {
case export.TeamsOption:
output = append(output, teamsPermissions...)
} else if exportOption == export.ResultsOption {
case export.ResultsOption:
output = append(output, resultsPermissions...)
}
}
Expand Down
2 changes: 1 addition & 1 deletion internal/app/resultsmapping/generate_csv.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,6 @@ func WriteAllToSanitizedCsv(records [][]string) []byte {
}

func sanitize(cell string) string {
escapedCell := strings.Replace(cell, `"`, `""`, -1)
escapedCell := strings.ReplaceAll(cell, `"`, `""`)
return fmt.Sprintf(`"'%s"`, escapedCell)
}
5 changes: 3 additions & 2 deletions internal/integration/rest/apiclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,8 @@ func (c *APIClient) Authenticate(username, password string) error {
Int("statusCode", resp.StatusCode).
Logger()

if resp.StatusCode == http.StatusOK {
switch resp.StatusCode {
case http.StatusOK:
responseBody, ioErr := io.ReadAll(resp.Body)
if ioErr != nil {
logger.Debug().Err(ioErr).Msg("authenticate ok failed read response")
Expand All @@ -128,7 +129,7 @@ func (c *APIClient) Authenticate(username, password string) error {
return fmt.Errorf("authentication error - could not decode response")
}
return nil
} else if resp.StatusCode == http.StatusBadRequest {
case http.StatusBadRequest:
responseBody, ioErr := io.ReadAll(resp.Body)
if ioErr != nil {
logger.Debug().Err(ioErr).Msg("authenticate bad request failed to read response")
Expand Down
106 changes: 94 additions & 12 deletions internal/process.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package internal

import (
"bytes"
"context"
"encoding/json"
"encoding/xml"
"fmt"
Expand Down Expand Up @@ -47,12 +48,13 @@ import (
)

const (
scansFileName = "%d.xml"
scansMetadataFileName = "%d.json"
resultsPageLimit = 10000
httpRetryWaitMin = 1 * time.Second //nolint:revive
httpRetryWaitMax = 30 * time.Second
httpRetryMax = 4
scansFileName = "%d.xml"
scansMetadataFileName = "%d.json"
resultsPageLimit = 10000
httpRetryWaitMin = 1 * time.Second //nolint:revive
httpRetryWaitMax = 30 * time.Second
httpRetryMax = 8
httpRateLimitRetryDelay = 200 * time.Millisecond

scanReportCreateAttempts = 10
scanReportCreateMinSleep = 1 * time.Second
Expand Down Expand Up @@ -1068,8 +1070,8 @@ func getRetryHTTPClient() *retryablehttp.Client {
RetryWaitMin: httpRetryWaitMin,
RetryWaitMax: httpRetryWaitMax,
RetryMax: httpRetryMax,
CheckRetry: retryablehttp.DefaultRetryPolicy,
Backoff: retryablehttp.DefaultBackoff,
CheckRetry: customRetryPolicy,
Backoff: customBackoff,
RequestLogHook: func(_ retryablehttp.Logger, request *http.Request, i int) {
log.Debug().
Str("method", request.Method).
Expand All @@ -1087,6 +1089,73 @@ func getRetryHTTPClient() *retryablehttp.Client {
}
}

// customRetryPolicy determines whether a request should be retried, with special handling for rate limiting
func customRetryPolicy(ctx context.Context, resp *http.Response, err error) (bool, error) {
// Use default policy first
shouldRetry, checkErr := retryablehttp.DefaultRetryPolicy(ctx, resp, err)

// If default policy says retry, return that
if shouldRetry {
return true, checkErr
}

// Additionally check for 429 Too Many Requests
if resp != nil && resp.StatusCode == http.StatusTooManyRequests {
log.Debug().
Str("url", resp.Request.URL.String()).
Msg("Rate limited (429), will retry with backoff")
return true, nil
}

return shouldRetry, checkErr
}

// customBackoff provides exponential backoff with special handling for rate limiting
func customBackoff(min, max time.Duration, attemptNum int, resp *http.Response) time.Duration {
// If we got a 429, check for Retry-After header first
if resp != nil && resp.StatusCode == http.StatusTooManyRequests {
// Check for Retry-After header (can be in seconds or HTTP date)
if retryAfter := resp.Header.Get("Retry-After"); retryAfter != "" {
// Try parsing as seconds first
if seconds, err := strconv.Atoi(retryAfter); err == nil && seconds > 0 {
delay := time.Duration(seconds) * time.Second
if delay > max {
delay = max
}
log.Debug().
Dur("delay", delay).
Int("attempt", attemptNum).
Str("retry_after", retryAfter).
Msg("Rate limit backoff delay from Retry-After header")
return delay
}
}

// If no Retry-After header, use exponential backoff starting at httpRateLimitRetryDelay
// Cap attemptNum to prevent overflow
safeAttempt := attemptNum
if safeAttempt > 10 {
safeAttempt = 10
}
baseDelay := httpRateLimitRetryDelay * time.Duration(1<<uint(safeAttempt))

// Cap at max
if baseDelay > max {
return max
}

log.Debug().
Dur("delay", baseDelay).
Int("attempt", attemptNum).
Msg("Rate limit backoff delay")

return baseDelay
}

// Otherwise use default exponential backoff
return retryablehttp.DefaultBackoff(min, max, attemptNum, resp)
}

func addQueryMappingFile(queryMappingProvider interfaces.QueryMappingRepo, exporter export2.Exporter) error {
mapping := querymapping.MapSource{
Mappings: queryMappingProvider.GetMapping(),
Expand Down Expand Up @@ -1176,7 +1245,9 @@ func fetchCustomExtensions(args *Args, exporter export2.Exporter) error {
// Split the input string by space to get language, extension and language group
parts := strings.Split(args.CustomExtensions, " ")
if len(parts) < 3 || len(parts)%3 != 0 {
return fmt.Errorf("invalid custom extensions format. Expected: Language Extension LanguageGroup [Language Extension LanguageGroup ...]")
return fmt.Errorf(
"invalid custom extensions format. Expected: Language Extension LanguageGroup [...]",
)
}

customExtensions := &CustomExtensionsList{
Expand Down Expand Up @@ -1214,7 +1285,12 @@ func fetchCustomExtensions(args *Args, exporter export2.Exporter) error {
return nil
}

func fetchProjectExcludeSettings(client rest.Client, exporter export2.Exporter, projects []*rest.Project, projectIDs string) error {
func fetchProjectExcludeSettings(
client rest.Client,
exporter export2.Exporter,
projects []*rest.Project,
projectIDs string,
) error {
var allExcludeSettings []*rest.ProjectExcludeSettings

// If specific project IDs are provided, filter the projects
Expand All @@ -1233,7 +1309,10 @@ func fetchProjectExcludeSettings(client rest.Client, exporter export2.Exporter,
log.Info().Int("projectID", project.ID).Msg("collecting project exclude settings")
excludeSettings, err := client.GetProjectExcludeSettings(project.ID)
if err != nil {
return errors.Wrapf(err, "failed to get exclude settings for project %d", project.ID)
log.Error().Err(err).
Int("projectID", project.ID).
Msg("Skipping project due to error getting exclude settings")
continue
}
allExcludeSettings = append(allExcludeSettings, excludeSettings)
}
Expand All @@ -1244,7 +1323,10 @@ func fetchProjectExcludeSettings(client rest.Client, exporter export2.Exporter,
log.Info().Int("projectID", project.ID).Msg("collecting project exclude settings")
excludeSettings, err := client.GetProjectExcludeSettings(project.ID)
if err != nil {
return errors.Wrapf(err, "failed to get exclude settings for project %d", project.ID)
log.Error().Err(err).
Int("projectID", project.ID).
Msg("Skipping project due to error getting exclude settings")
continue
}
allExcludeSettings = append(allExcludeSettings, excludeSettings)
}
Expand Down
Loading