Skip to content

[PPSC-678] fix: graceful degradation when result fetching fails#141

Merged
shb7628 merged 4 commits intomainfrom
fix/PPSC-678-increase-http-timeout
Apr 13, 2026
Merged

[PPSC-678] fix: graceful degradation when result fetching fails#141
shb7628 merged 4 commits intomainfrom
fix/PPSC-678-increase-http-timeout

Conversation

@shb7628
Copy link
Copy Markdown
Collaborator

@shb7628 shb7628 commented Apr 13, 2026

Summary

  • Instead of failing the entire scan when normalized result retrieval times out, retry up to 5 times with 10s intervals (transient errors only)
  • If all retries fail, exit with code 2 (not 0) and print scan ID so users can retrieve results later
  • Applied to both repo and image scan paths

Problem

The CLI reported "scan failed" (exit 1) when the /api/v1/ingest/normalized-results HTTP request timed out, even though the scan completed successfully on the server. The scan ID was lost in the error, leaving users with no way to retrieve their results. In CI, this was indistinguishable from an actual scan failure.

This is distinct from slow scans (e.g., due to Cerebras LLM latency in the AI verifier). Slow server-side processing is handled correctly by the polling loop in WaitForIngest, which polls every 5s for up to 60 minutes. The failure only occurred in the result-fetching phase after the scan was already marked COMPLETED.

Solution

  1. Retry with error discrimination: Retry result fetching up to 5 times with 10s intervals, but only for transient errors (5xx, timeouts, network errors). Permanent errors (4xx, decode failures) fail immediately. Each attempt also has 3 internal HTTP retries with 60s timeout, giving ~15 min total window for transient issues.

  2. Non-zero exit code: On total failure, return ErrResultsIncomplete (exit code 2) instead of nil (exit 0). CI pipelines will not silently pass when results are unavailable. Exit codes: 0 = success, 1 = findings exceeded --fail-on threshold, 2 = results incomplete, 130 = cancelled.

  3. User feedback: Print warnings with the scan ID so users can retrieve results later. Both repo and image scans show retry progress via spinner.

Changes

  • internal/api/client.go: Added APIError type for structured HTTP status code checks
  • internal/output/output.go: Added ErrResultsIncomplete error type
  • cmd/armis-cli/main.go: Handle ErrResultsIncomplete with exit code 2
  • internal/scan/repo/repo.go: Retry loop with error discrimination and graceful degradation
  • internal/scan/image/image.go: Same retry logic + spinner feedback (was missing)
  • Tests updated for both repo and image scanners

Test plan

  • go build ./... compiles successfully
  • go test ./internal/scan/repo/... passes (new test for ErrResultsIncomplete)
  • go test ./internal/scan/image/... passes (updated test)
  • go test ./internal/api/... passes
  • go test ./internal/output/... passes
  • Security scan — no new findings
  • Manual test: run a scan against MooseProd and verify exit code 2 on timeout

🤖 Generated with Claude Code

The per-request HTTP client timeout was hardcoded at 60 seconds, causing
"context deadline exceeded" errors when fetching normalized results under
network latency or load. Increased to 180 seconds to match the ALB idle
timeout and provide more headroom for result retrieval.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 13, 2026

Armis AppSecArmis AppSec Security Scan Results

🟠 HIGH issues found

Severity Count
🟠 HIGH 3

Total: 3

View all 3 findings

🟠 HIGH (3)

CWE-522_armis-cli_38295677_internal/api/client.go_744_2_744_53 - Insecure Design (CWE-522

Location: internal/api/client.go:744

Insufficiently Protected Credentials): The ValidatePresignedURL function checks a URL before the client downloads it. If the URL points to localhost or 127.0.0.1 and the client was created with the allowLocalURLs option set to true, the function skips the HTTPS check and returns success. The download code later uses this URL directly in an HTTP request, sending the full presigned URL (which contains authentication signatures) to whatever service is listening on the local address. Because the check does not require a secure HTTPS connection for local URLs, an attacker who can run a malicious service on the same machine could receive the presigned URL and read the embedded credentials. The URL that reaches the download step comes from the function argument, which could be supplied by external code or a user, so the risky path is reachable. No additional validation or sanitization is performed for the localhost case, leaving the credentials exposed to any local listener.

CWEs: CWE-522: Insufficiently Protected Credentials

CWE-522_armis-cli_38295677_internal/api/client.go_242_2_242_78 - Insecure Design (CWE-522

Location: internal/api/client.go:242

Insufficiently Protected Credentials): The setAuthHeader function builds the HTTP request that the CLI sends to the Armis service. It first checks the request’s host and scheme. If the host is not localhost or 127.0.0.1, it requires HTTPS. However, when the host is localhost (or the loop‑back IP), the function does not require HTTPS, so an HTTP request is allowed. After the check, the function obtains the authorization value from the authProvider (this is where the token or password comes from) and puts it into the Authorization header of the request. Because the check lets HTTP be used for localhost, the credentials can be sent over an unencrypted connection. If an attacker can cause the client to contact a local server (for example, by influencing the URL that the client uses), the credentials will travel in clear text and could be captured by any process that can sniff local network traffic. The code does not add any extra protection for this case; the only safeguard is the comment that the exception is meant for testing. In short, the function can expose authentication data when it talks to a localhost address over plain HTTP, which matches the description of CWE‑522. The risk exists whenever the URL can be set to a local address, and there is no additional validation to prevent it.

CWEs: CWE-522: Insufficiently Protected Credentials

CWE-22_armis-cli_38295677_internal/scan/image/image.go_136_2_136_87 - Broken Access Control (CWE-22

Location: internal/scan/image/image.go:136

Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')): The function ScanTarball receives a file path (tarballPath) and opens it with os.Open. Before opening, it calls util.SanitizePath to try to clean the path, but the code does not show how that cleaning works. Because the cleaning step might not stop an attacker from using sequences like ../ to move out of the intended directory, a malicious user could supply a path that points to any file the program can read. If that happens, the program would open and later upload the contents of that file, potentially exposing sensitive data. The function is part of a command‑line tool, so a user can provide the path directly when invoking the tool, making the risky operation reachable from external input. No additional checks are performed after the sanitization step, so the protection is incomplete. Adding a stricter validation that ensures the path stays inside a safe directory (or using a whitelist of allowed locations) would remove the possibility of path traversal.

CWEs: CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')

@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 13, 2026

Test Coverage Report

total: (statements) 80.9%

Coverage by function
github.com/ArmisSecurity/armis-cli/cmd/armis-cli/main.go:19:			main					0.0%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:29:			Error					0.0%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:72:			copyWithContext				70.4%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:145:			WithHTTPClient				100.0%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:154:			WithUploadHTTPClient			100.0%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:162:			WithAllowLocalURLs			100.0%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:174:			NewClient				100.0%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:222:			IsDebug					100.0%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:236:			setAuthHeader				77.8%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:270:			StartIngest				72.3%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:429:			GetIngestStatus				82.6%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:470:			WaitForIngest				84.6%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:521:			FetchNormalizedResults			74.2%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:576:			FetchAllNormalizedResults		91.7%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:601:			GetScanResult				68.4%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:636:			WaitForScan				90.0%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:657:			formatBytes				100.0%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:679:			FetchArtifactScanResults		75.0%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:734:			ValidatePresignedURL			100.0%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:770:			DownloadFromPresignedURL		84.2%
github.com/ArmisSecurity/armis-cli/internal/auth/auth.go:58:			NewAuthProvider				95.2%
github.com/ArmisSecurity/armis-cli/internal/auth/auth.go:104:			GetAuthorizationHeader			100.0%
github.com/ArmisSecurity/armis-cli/internal/auth/auth.go:124:			GetTenantID				85.7%
github.com/ArmisSecurity/armis-cli/internal/auth/auth.go:141:			GetRegion				85.7%
github.com/ArmisSecurity/armis-cli/internal/auth/auth.go:156:			IsLegacy				100.0%
github.com/ArmisSecurity/armis-cli/internal/auth/auth.go:169:			GetRawToken				85.7%
github.com/ArmisSecurity/armis-cli/internal/auth/auth.go:196:			exchangeCredentials			87.9%
github.com/ArmisSecurity/armis-cli/internal/auth/auth.go:267:			refreshIfNeeded				100.0%
github.com/ArmisSecurity/armis-cli/internal/auth/auth.go:297:			parseJWTClaims				93.3%
github.com/ArmisSecurity/armis-cli/internal/auth/client.go:29:			Error					100.0%
github.com/ArmisSecurity/armis-cli/internal/auth/client.go:41:			NewAuthClient				100.0%
github.com/ArmisSecurity/armis-cli/internal/auth/client.go:97:			Authenticate				77.4%
github.com/ArmisSecurity/armis-cli/internal/auth/region_cache.go:34:		NewRegionCache				100.0%
github.com/ArmisSecurity/armis-cli/internal/auth/region_cache.go:40:		Load					82.4%
github.com/ArmisSecurity/armis-cli/internal/auth/region_cache.go:74:		Save					76.9%
github.com/ArmisSecurity/armis-cli/internal/auth/region_cache.go:104:		Clear					75.0%
github.com/ArmisSecurity/armis-cli/internal/auth/region_cache.go:114:		getFilePath				83.3%
github.com/ArmisSecurity/armis-cli/internal/auth/region_cache.go:131:		loadCachedRegion			100.0%
github.com/ArmisSecurity/armis-cli/internal/auth/region_cache.go:135:		saveCachedRegion			100.0%
github.com/ArmisSecurity/armis-cli/internal/auth/region_cache.go:139:		clearCachedRegion			100.0%
github.com/ArmisSecurity/armis-cli/internal/cli/color.go:60:			InitColors				85.2%
github.com/ArmisSecurity/armis-cli/internal/cli/color.go:107:			ColorsEnabled				100.0%
github.com/ArmisSecurity/armis-cli/internal/cli/color.go:113:			ColorsForced				100.0%
github.com/ArmisSecurity/armis-cli/internal/cli/color.go:119:			SetOutputToFile				100.0%
github.com/ArmisSecurity/armis-cli/internal/cli/color.go:125:			GetOutputToFile				0.0%
github.com/ArmisSecurity/armis-cli/internal/cli/color.go:129:			enableColors				100.0%
github.com/ArmisSecurity/armis-cli/internal/cli/color.go:136:			disableColors				100.0%
github.com/ArmisSecurity/armis-cli/internal/cli/color.go:151:			parseErrorMessage			92.9%
github.com/ArmisSecurity/armis-cli/internal/cli/color.go:182:			PrintError				100.0%
github.com/ArmisSecurity/armis-cli/internal/cli/color.go:195:			PrintErrorf				0.0%
github.com/ArmisSecurity/armis-cli/internal/cli/color.go:201:			PrintWarning				100.0%
github.com/ArmisSecurity/armis-cli/internal/cli/color.go:206:			PrintWarningf				100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/auth.go:33:			init					100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/auth.go:39:			runAuth					92.9%
github.com/ArmisSecurity/armis-cli/internal/cmd/context.go:24:			NewSignalContext			100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/context.go:33:			handleScanError				100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/help.go:30:			SetupHelp				91.7%
github.com/ArmisSecurity/armis-cli/internal/cmd/help.go:58:			styledUsageTemplate			100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/help.go:101:			defaultUsageTemplate			100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/help.go:108:			initColorsForHelp			35.3%
github.com/ArmisSecurity/armis-cli/internal/cmd/help.go:149:			styleHelpOutput				83.3%
github.com/ArmisSecurity/armis-cli/internal/cmd/output_helper.go:27:		Cleanup					100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/output_helper.go:53:		ResolveOutput				96.4%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:149:			SetVersion				100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:157:			Execute					100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:161:			init					100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:192:			PrintUpdateNotification			81.2%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:234:			printUpdateNotificationOnce		75.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:247:			getEnvOrDefault				100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:254:			getEnvOrDefaultInt			100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:264:			getAPIBaseURL				100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:277:			getAuthProvider				100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:289:			getPageLimit				100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:296:			validatePageLimit			100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:306:			validateFailOn				100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:324:			getFailOn				100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/scan.go:92:			init					100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/scan_image.go:152:		init					100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/scan_repo.go:188:		init					100.0%
github.com/ArmisSecurity/armis-cli/internal/httpclient/client.go:31:		NewClient				100.0%
github.com/ArmisSecurity/armis-cli/internal/httpclient/client.go:57:		Do					86.1%
github.com/ArmisSecurity/armis-cli/internal/output/errno_unix.go:12:		isSyncNotSupported			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:54:			wrapText				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:77:			wrapLine				91.7%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:115:		formatRecommendations			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:185:		wrapTextWithFirstLinePrefix		90.9%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:224:		write					66.7%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:255:		Write					89.5%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:285:		Format					100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:290:		FormatWithOptions			84.4%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:360:		SyncColors				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:364:		sortFindingsBySeverity			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:375:		loadSnippetFromFile			69.4%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:487:		formatCodeSnippetWithFrame		91.1%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:580:		truncatePlainLine			0.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:592:		highlightColumns			93.5%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:637:		scanDuration				89.5%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:670:		pluralize				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:679:		renderBriefStatus			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:719:		renderSummaryDashboard			56.4%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:800:		renderFindings				88.9%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:829:		renderFinding				69.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:919:		renderGroupedFindings			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:943:		groupFindings				96.8%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1000:		severityRank				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1007:		isGitRepo				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1014:		getGitBlame				38.1%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1051:		parseGitBlame				95.2%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1087:		maskEmail				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1110:		getTopLevelDomain			75.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1122:		getHumanDisplayTitle			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1136:		wrapTitle				93.9%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1195:		maskFixForDisplay			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1230:		formatFixSection			0.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1295:		formatProposedSnippet			0.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1378:		limitHunkContext			64.7%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1454:		parseDiffHunk				91.7%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1476:		parseDiffLines				94.6%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1567:		findInlineChanges			73.5%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1638:		computeLCS				92.3%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1690:		buildTokenPositions			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1706:		tokenizeLine				92.9%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1734:		isWordChar				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1741:		formatDiffWithColorsStyled		77.1%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1815:		extractDiffFilename			80.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1837:		formatDiffHunkLine			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1857:		formatDiffContextLine			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1868:		formatDiffRemoveLine			86.4%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1909:		formatDiffAddLine			86.4%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1951:		applyInlineHighlights			81.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1993:		truncateDiffLine			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:2000:		truncateDiffLineWithFlag		66.7%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:2014:		adjustHighlightSpans			83.3%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:2036:		groupDiffHunks				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:2067:		collectRenderOps			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:2110:		renderChangeBlock			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:2169:		formatDiffHunkSeparator			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:2184:		formatValidationSection			0.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:2241:		getExposureDescription			0.0%
github.com/ArmisSecurity/armis-cli/internal/output/icons.go:24:			GetConfidenceIcon			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/json.go:15:			Format					100.0%
github.com/ArmisSecurity/armis-cli/internal/output/json.go:24:			FormatWithOptions			66.7%
github.com/ArmisSecurity/armis-cli/internal/output/json.go:32:			formatWithDebug				0.0%
github.com/ArmisSecurity/armis-cli/internal/output/json.go:58:			maskScanResultForOutput			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/json.go:78:			maskFindingSecrets			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/junit.go:48:			Format					100.0%
github.com/ArmisSecurity/armis-cli/internal/output/junit.go:55:			FormatWithOptions			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/junit.go:63:			formatWithSeverities			83.3%
github.com/ArmisSecurity/armis-cli/internal/output/junit.go:88:			isFailureSeverity			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/junit.go:98:			convertToJUnitCasesWithSeverities	91.7%
github.com/ArmisSecurity/armis-cli/internal/output/junit.go:130:		countFailuresWithSeverities		100.0%
github.com/ArmisSecurity/armis-cli/internal/output/output.go:24:		Error					0.0%
github.com/ArmisSecurity/armis-cli/internal/output/output.go:35:		Error					0.0%
github.com/ArmisSecurity/armis-cli/internal/output/output.go:55:		GetFormatter				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/output.go:71:		ShouldFail				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/output.go:89:		CheckExit				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/sarif.go:159:		stripMarkdown				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/sarif.go:170:		Format					100.0%
github.com/ArmisSecurity/armis-cli/internal/output/sarif.go:197:		buildRules				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/sarif.go:261:		convertToSarifResults			88.5%
github.com/ArmisSecurity/armis-cli/internal/output/sarif.go:351:		buildMessageText			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/sarif.go:358:		severityToSarifLevel			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/sarif.go:377:		severityToSecurityScore			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/sarif.go:395:		generateHelpURI				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/sarif.go:422:		convertFixToSarif			90.5%
github.com/ArmisSecurity/armis-cli/internal/output/sarif.go:539:		FormatWithOptions			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/styles.go:138:		DefaultStyles				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/styles.go:276:		NoColorStyles				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/styles.go:353:		GetStyles				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/styles.go:361:		SyncStylesWithColorMode			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/styles.go:386:		GetSeverityText				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/styles.go:414:		TerminalWidth				33.3%
github.com/ArmisSecurity/armis-cli/internal/output/syntax.go:21:		GetLexer				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/syntax.go:32:		GetChromaStyle				80.0%
github.com/ArmisSecurity/armis-cli/internal/output/syntax.go:45:		HighlightCode				81.2%
github.com/ArmisSecurity/armis-cli/internal/output/syntax.go:79:		HighlightLine				75.0%
github.com/ArmisSecurity/armis-cli/internal/output/syntax.go:88:		getTerminalFormatter			60.0%
github.com/ArmisSecurity/armis-cli/internal/output/syntax.go:103:		HighlightLineWithBackground		87.5%
github.com/ArmisSecurity/armis-cli/internal/output/syntax.go:126:		getBackgroundANSI			58.3%
github.com/ArmisSecurity/armis-cli/internal/output/syntax.go:158:		rgbToANSI256				0.0%
github.com/ArmisSecurity/armis-cli/internal/output/syntax.go:171:		parseHexColor				76.9%
github.com/ArmisSecurity/armis-cli/internal/output/writer.go:51:		validateOutputPath			92.3%
github.com/ArmisSecurity/armis-cli/internal/output/writer.go:88:		NewFileOutput				88.2%
github.com/ArmisSecurity/armis-cli/internal/output/writer.go:142:		Writer					100.0%
github.com/ArmisSecurity/armis-cli/internal/output/writer.go:147:		Close					100.0%
github.com/ArmisSecurity/armis-cli/internal/output/writer.go:164:		FormatFromExtension			100.0%
github.com/ArmisSecurity/armis-cli/internal/progress/progress.go:32:		IsCI					100.0%
github.com/ArmisSecurity/armis-cli/internal/progress/progress.go:60:		isTerminalWriter			100.0%
github.com/ArmisSecurity/armis-cli/internal/progress/progress.go:68:		NewReader				100.0%
github.com/ArmisSecurity/armis-cli/internal/progress/progress.go:83:		NewWriter				50.0%
github.com/ArmisSecurity/armis-cli/internal/progress/progress.go:117:		NewSpinner				100.0%
github.com/ArmisSecurity/armis-cli/internal/progress/progress.go:125:		NewSpinnerWithTimeout			100.0%
github.com/ArmisSecurity/armis-cli/internal/progress/progress.go:141:		NewSpinnerWithContext			100.0%
github.com/ArmisSecurity/armis-cli/internal/progress/progress.go:149:		SetWriter				100.0%
github.com/ArmisSecurity/armis-cli/internal/progress/progress.go:158:		Start					89.8%
github.com/ArmisSecurity/armis-cli/internal/progress/progress.go:268:		Stop					100.0%
github.com/ArmisSecurity/armis-cli/internal/progress/progress.go:303:		Update					100.0%
github.com/ArmisSecurity/armis-cli/internal/progress/progress.go:310:		GetElapsed				100.0%
github.com/ArmisSecurity/armis-cli/internal/progress/progress.go:317:		formatDuration				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/finding_type.go:9:		DeriveFindingType			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:48:		NewScanner				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:63:		WithPollInterval			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:69:		WithFetchRetryInterval			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:75:		WithSBOMVEXOptions			0.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:82:		WithPullPolicy				0.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:88:		ScanImage				0.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:119:		ScanTarball				77.8%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:230:		exportImage				0.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:280:		isDockerAvailable			42.9%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:294:		getDockerCommand			75.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:303:		validateDockerCommand			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:311:		imageExistsLocally			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:319:		determinePullBehavior			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:337:		isRetryableError			75.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:345:		buildScanResult				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:372:		convertNormalizedFindings		85.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:495:		shouldFilterByExploitability		100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:514:		cleanDescription			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:533:		isEmptyFinding				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:548:		generateFindingTitle			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/validate.go:11:		validateImageName			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/mask.go:21:			MaskFixSecrets				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/files.go:26:		ParseFileList				87.5%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/files.go:41:		addFile					87.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/files.go:93:		Files					100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/files.go:98:		RepoRoot				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/files.go:103:		ValidateExistence			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/gitchanges.go:52:		GitChangedFiles				82.6%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/gitchanges.go:102:	gitRepoRoot				80.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/gitchanges.go:126:	changedUncommitted			41.7%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/gitchanges.go:155:	changedStaged				75.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/gitchanges.go:168:	validateRef				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/gitchanges.go:181:	changedSinceRef				75.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/gitchanges.go:204:	filterToScanPath			94.1%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/gitchanges.go:239:	runGit					91.7%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/gitchanges.go:265:	parseLines				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/gitchanges.go:285:	combineAndDedupe			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/ignore.go:23:		LoadIgnorePatterns			88.9%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/ignore.go:60:		loadIgnoreFile				88.5%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/ignore.go:105:		Match					100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/ignore.go:117:		shouldSkipDir				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:46:		NewScanner				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:61:		WithPollInterval			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:67:		WithFetchRetryInterval			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:73:		WithIncludeFiles			0.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:79:		WithSBOMVEXOptions			0.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:85:		Scan					72.7%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:267:		tarGzDirectory				71.8%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:350:		isPathContained				75.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:359:		tarGzFiles				78.6%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:450:		safeAddSize				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:457:		calculateFilesSize			78.6%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:482:		calculateDirSize			79.2%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:526:		shouldSkip				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:557:		isTestFile				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:603:		isRetryableError			75.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:612:		buildScanResult				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:639:		convertNormalizedFindings		73.3%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:762:		shouldFilterByExploitability		100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:781:		cleanDescription			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:802:		generateFindingTitle			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:806:		isEmptyFinding				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/sbom_vex.go:38:		NewSBOMVEXDownloader			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/sbom_vex.go:50:		Download				85.2%
github.com/ArmisSecurity/armis-cli/internal/scan/sbom_vex.go:102:		downloadAndSave				77.8%
github.com/ArmisSecurity/armis-cli/internal/scan/status.go:16:			FormatScanStatus			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/status.go:35:			FormatElapsed				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/status.go:48:			MapSeverity				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/testhelpers/findings.go:9:	CreateNormalizedFinding			0.0%
github.com/ArmisSecurity/armis-cli/internal/scan/testhelpers/findings.go:14:	CreateNormalizedFindingWithLabels	0.0%
github.com/ArmisSecurity/armis-cli/internal/scan/testhelpers/findings.go:19:	CreateNormalizedFindingFull		0.0%
github.com/ArmisSecurity/armis-cli/internal/scan/title.go:14:			GenerateFindingTitle			0.0%
github.com/ArmisSecurity/armis-cli/internal/update/update.go:63:		NewChecker				100.0%
github.com/ArmisSecurity/armis-cli/internal/update/update.go:79:		CheckCached				100.0%
github.com/ArmisSecurity/armis-cli/internal/update/update.go:97:		CheckInBackground			100.0%
github.com/ArmisSecurity/armis-cli/internal/update/update.go:117:		check					85.7%
github.com/ArmisSecurity/armis-cli/internal/update/update.go:160:		fetchLatestVersion			89.5%
github.com/ArmisSecurity/armis-cli/internal/update/update.go:193:		getCacheFilePath			66.7%
github.com/ArmisSecurity/armis-cli/internal/update/update.go:208:		readCache				84.6%
github.com/ArmisSecurity/armis-cli/internal/update/update.go:231:		writeCache				76.9%
github.com/ArmisSecurity/armis-cli/internal/update/update.go:254:		IsNewer					100.0%
github.com/ArmisSecurity/armis-cli/internal/update/update.go:277:		parseVersion				100.0%
github.com/ArmisSecurity/armis-cli/internal/update/update.go:300:		FormatNotification			100.0%
github.com/ArmisSecurity/armis-cli/internal/update/update.go:319:		getUpdateCommand			40.0%
github.com/ArmisSecurity/armis-cli/internal/util/cache.go:21:			GetCacheDir				75.0%
github.com/ArmisSecurity/armis-cli/internal/util/cache.go:41:			GetCacheFilePath			80.0%
github.com/ArmisSecurity/armis-cli/internal/util/format.go:7:			FormatCategory				100.0%
github.com/ArmisSecurity/armis-cli/internal/util/mask.go:109:			MaskSecretInLine			86.4%
github.com/ArmisSecurity/armis-cli/internal/util/mask.go:163:			maskValue				83.3%
github.com/ArmisSecurity/armis-cli/internal/util/mask.go:189:			MaskSecretInLines			100.0%
github.com/ArmisSecurity/armis-cli/internal/util/mask.go:203:			MaskSecretInMultiLineString		100.0%
github.com/ArmisSecurity/armis-cli/internal/util/mask.go:217:			MaskSecretsInStringMap			100.0%
github.com/ArmisSecurity/armis-cli/internal/util/path.go:13:			SanitizePath				90.9%
github.com/ArmisSecurity/armis-cli/internal/util/path.go:51:			SafeJoinPath				87.5%
github.com/ArmisSecurity/armis-cli/test/sample-repo/src/main.go:6:		main					0.0%
total:										(statements)				80.9%

Instead of failing the entire scan when normalized result retrieval
times out, retry up to 5 times with 10s intervals (total ~50s window).
If all retries fail, print a warning with the scan ID so the user can
retrieve results later, rather than reporting "scan failed" when the
scan actually completed successfully on the server.

Also reverts the timeout bump from 60s back to the original value since
the retry + graceful degradation is the proper fix.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@shb7628 shb7628 changed the title [PPSC-678] fix: increase HTTP client timeout to 180s [PPSC-678] fix: More gracefully handle http retries on results Apr 13, 2026
@shb7628 shb7628 changed the title [PPSC-678] fix: More gracefully handle http retries on results [PPSC-678] fix: graceful degradation when result fetching fails Apr 13, 2026
shb7628 and others added 2 commits April 13, 2026 12:37
1. Exit code 2 on incomplete results: Instead of returning nil error
   (exit 0) when results can't be fetched, return ErrResultsIncomplete
   which main.go handles with exit code 2. CI pipelines will not
   silently pass when results are unavailable.

2. Error discrimination: Only retry on transient errors (5xx, timeouts,
   network errors). Permanent errors (4xx, decode failures) break
   immediately instead of wasting ~50s retrying pointlessly. Added
   APIError type to the api package for structured status code checks.

3. Repo scanner test: Added TestScan/returns_ErrResultsIncomplete test
   to match the existing image scanner test coverage.

4. Image scan retry feedback: Added spinner with retry progress
   messages to image scans, matching the repo scan behavior.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ests

Fixes goconst lint failure by extracting "scan-123" into a testScanID constant.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@shb7628 shb7628 merged commit 78f04f2 into main Apr 13, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant