Skip to content

Conversation

@yiftach-armis
Copy link
Collaborator

@yiftach-armis yiftach-armis commented Feb 11, 2026

Related Issue

Type of Change

  • New feature (non-breaking change which adds functionality)

Problem

The armis-cli needed improved usability features to better handle output formatting and keep users informed about available updates.

Solution

This PR adds two major features to improve CLI usability:

  1. Color Output Management (internal/cli/color.go)

    • Intelligent TTY detection on stderr for automatic color support
    • Respects NO_COLOR environment variable
    • Provides --color flag with auto/always/never modes
    • Follows industry standards for color handling
  2. Version Update Checking (internal/update/update.go)

    • Checks GitHub releases for newer versions asynchronously
    • Implements local caching (24-hour TTL) to avoid excessive API calls
    • Non-blocking checks that don't impact CLI performance
    • Notifies users of available updates during command execution

Additional improvements:

  • Enhanced root command with color flag integration
  • Improved progress handling in scan operations
  • Better user feedback with colored output for warnings and errors

Testing

Automated Tests

  • Unit tests added/updated
  • All tests passing locally
  • Pre-commit hooks passing (golangci-lint, go fmt, go vet)

Manual Testing

Verified by running:

  • make test - All tests pass
  • make lint - No linting errors
  • CLI with various flags: armis-cli --help, armis-cli --color=never, etc.

Reviewer Notes

  • Color detection uses golang.org/x/term for cross-platform TTY detection
  • Update checks are non-blocking and cached to avoid performance impact
  • All new code includes comprehensive test coverage
  • No breaking changes to existing functionality

Checklist

  • Code follows project style guidelines
  • Pre-commit hooks pass
  • Self-review performed
  • No new warnings generated

…date checks

- Add color output management with TTY detection and NO_COLOR support
- Implement version update checking with GitHub releases API and local caching
- Update root command with --color flag and version check integration
- Enhance scan commands with better progress handling and user feedback
- Add comprehensive tests for new features
Copilot AI review requested due to automatic review settings February 11, 2026 15:40
@github-actions
Copy link

github-actions bot commented Feb 11, 2026

🛡️ Armis Security Scan Results

🟠 HIGH issues found

Severity Count
🟠 HIGH 4
🟡 MEDIUM 1

Total: 5

View all 5 findings

🟠 HIGH (4)

CWE-770_armis-cli_internal/api/client.go_237_2_237_52 - Data from opts.Data is copied into a multipart buffer without size limiting, ...

Location: internal/api/client.go:237

Data from opts.Data is copied into a multipart buffer without size limiting, potentially consuming excessive memory if the actual data exceeds expected limits.

CWEs: CWE-770: Allocation of Resources Without Limits or Throttling

CWE-319_armis-cli_internal/api/client.go_159_2_159_78 - Cryptography Failures (CWE-319

Location: internal/api/client.go:159

Cleartext Transmission of Sensitive Information): The credential Authorization header is allowed to be sent over non‑HTTPS schemes for localhost hosts, risking exposure of sensitive authentication data in cleartext.

Code snippet is redacted as it contains secrets.

CWEs: CWE-319: Cleartext Transmission of Sensitive Information

CWE-918_armis-cli_internal/api/client.go_591_3_591_23 - Server-Side Request Forgery (CWE-918

Location: internal/api/client.go:591

Server-Side Request Forgery (SSRF)): When allowLocalURLs is enabled, the function returns nil for localhost/127.0.0.1 URLs without validating the scheme, permitting arbitrary HTTP requests to internal services and enabling SSRF attacks.

CWEs: CWE-918: Server-Side Request Forgery (SSRF)

CWE-22_armis-cli_internal/scan/image/image.go_118_2_118_87 - Broken Access Control (CWE-22

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

Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')): The code opens a file path supplied by the user (tarballPath) using os.Open after a call to util.SanitizePath. If the sanitization is insufficient, an attacker could perform a path traversal to access arbitrary files on the filesystem.

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

🟡 MEDIUM (1)

CWE-252_armis-cli_internal/cmd/context.go_26_3_26_55 - The return value (error) from fmt.Fprintln is ignored, so failures when writi...

Location: internal/cmd/context.go:26

The return value (error) from fmt.Fprintln is ignored, so failures when writing to standard error are not detected.

CWEs: CWE-252: Unchecked Return Value

@github-actions
Copy link

github-actions bot commented Feb 11, 2026

Test Coverage Report

total: (statements) 79.9%

Coverage by function
github.com/ArmisSecurity/armis-cli/cmd/armis-cli/main.go:17:			main					0.0%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:65:			WithHTTPClient				100.0%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:74:			WithAllowLocalURLs			100.0%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:86:			NewClient				100.0%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:136:			IsDebug					100.0%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:153:			setAuthHeader				77.8%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:187:			StartIngest				73.6%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:284:			GetIngestStatus				82.6%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:325:			WaitForIngest				83.3%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:372:			FetchNormalizedResults			75.9%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:422:			FetchAllNormalizedResults		91.7%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:447:			GetScanResult				68.4%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:482:			WaitForScan				90.0%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:503:			formatBytes				100.0%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:525:			FetchArtifactScanResults		75.0%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:580:			ValidatePresignedURL			100.0%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:616:			DownloadFromPresignedURL		84.2%
github.com/ArmisSecurity/armis-cli/internal/auth/auth.go:52:			NewAuthProvider				95.2%
github.com/ArmisSecurity/armis-cli/internal/auth/auth.go:98:			GetAuthorizationHeader			100.0%
github.com/ArmisSecurity/armis-cli/internal/auth/auth.go:118:			GetTenantID				85.7%
github.com/ArmisSecurity/armis-cli/internal/auth/auth.go:133:			IsLegacy				100.0%
github.com/ArmisSecurity/armis-cli/internal/auth/auth.go:146:			GetRawToken				85.7%
github.com/ArmisSecurity/armis-cli/internal/auth/auth.go:163:			exchangeCredentials			83.3%
github.com/ArmisSecurity/armis-cli/internal/auth/auth.go:193:			refreshIfNeeded				100.0%
github.com/ArmisSecurity/armis-cli/internal/auth/auth.go:222:			parseJWTClaims				93.3%
github.com/ArmisSecurity/armis-cli/internal/auth/client.go:31:			NewAuthClient				100.0%
github.com/ArmisSecurity/armis-cli/internal/auth/client.go:72:			Authenticate				71.0%
github.com/ArmisSecurity/armis-cli/internal/cli/color.go:41:			InitColors				69.2%
github.com/ArmisSecurity/armis-cli/internal/cli/color.go:65:			ColorsEnabled				100.0%
github.com/ArmisSecurity/armis-cli/internal/cli/color.go:69:			enableColors				100.0%
github.com/ArmisSecurity/armis-cli/internal/cli/color.go:77:			disableColors				100.0%
github.com/ArmisSecurity/armis-cli/internal/cli/color.go:87:			PrintError				100.0%
github.com/ArmisSecurity/armis-cli/internal/cli/color.go:92:			PrintErrorf				0.0%
github.com/ArmisSecurity/armis-cli/internal/cli/color.go:98:			PrintWarning				100.0%
github.com/ArmisSecurity/armis-cli/internal/cli/color.go:103:			PrintWarningf				100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/auth.go:27:			init					100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/auth.go:31:			runAuth					93.8%
github.com/ArmisSecurity/armis-cli/internal/cmd/context.go:16:			NewSignalContext			100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/context.go:23:			handleScanError				100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:87:			SetVersion				100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:95:			Execute					100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:99:			init					100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:123:			PrintUpdateNotification			0.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:140:			getEnvOrDefault				100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:147:			getEnvOrDefaultInt			100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:157:			getAPIBaseURL				100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:164:			getToken				100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:171:			getTenantID				100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:180:			getAuthProvider				100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:191:			getPageLimit				100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:198:			validatePageLimit			100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:208:			validateFailOn				100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:222:			getFailOn				100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/scan.go:27:			init					100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/scan_image.go:118:		init					100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/scan_repo.go:113:		init					100.0%
github.com/ArmisSecurity/armis-cli/internal/httpclient/client.go:30:		NewClient				100.0%
github.com/ArmisSecurity/armis-cli/internal/httpclient/client.go:56:		Do					85.3%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:36:			write					66.7%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:67:			Write					90.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:98:			Format					100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:103:		FormatWithOptions			78.6%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:173:		getSeverityIcon				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:190:		getSeverityColor			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:222:		SyncColors				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:230:		enableColors				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:243:		disableColors				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:256:		sortFindingsBySeverity			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:285:		loadSnippetFromFile			69.4%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:395:		formatCodeSnippet			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:432:		highlightColumns			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:468:		detectLanguage				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:766:		scanDuration				89.5%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:799:		pluralize				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:808:		renderBriefStatus			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:842:		renderSummaryDashboard			61.2%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:931:		renderFindings				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:946:		renderFinding				61.1%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1013:		renderGroupedFindings			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1034:		groupFindings				96.6%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1089:		severityRank				75.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1103:		isGitRepo				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1110:		getGitBlame				14.3%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1147:		parseGitBlame				95.2%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1183:		maskEmail				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1206:		getTopLevelDomain			75.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1215:		formatFixSection			0.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1266:		formatProposedSnippet			0.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1291:		formatDiffWithColors			0.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1311:		formatValidationSection			0.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1347:		getExposureDescription			0.0%
github.com/ArmisSecurity/armis-cli/internal/output/json.go:14:			Format					100.0%
github.com/ArmisSecurity/armis-cli/internal/output/json.go:21:			FormatWithOptions			66.7%
github.com/ArmisSecurity/armis-cli/internal/output/json.go:29:			formatWithDebug				0.0%
github.com/ArmisSecurity/armis-cli/internal/output/junit.go:43:			Format					83.3%
github.com/ArmisSecurity/armis-cli/internal/output/junit.go:67:			convertToJUnitCases			91.7%
github.com/ArmisSecurity/armis-cli/internal/output/junit.go:99:			countFailures				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/junit.go:112:		FormatWithOptions			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/output.go:33:		GetFormatter				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/output.go:49:		ShouldFail				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/output.go:65:		ExitIfNeeded				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:535:		FormatWithOptions			100.0%
github.com/ArmisSecurity/armis-cli/internal/progress/progress.go:25:		IsCI					100.0%
github.com/ArmisSecurity/armis-cli/internal/progress/progress.go:47:		NewReader				100.0%
github.com/ArmisSecurity/armis-cli/internal/progress/progress.go:62:		NewWriter				50.0%
github.com/ArmisSecurity/armis-cli/internal/progress/progress.go:96:		NewSpinner				100.0%
github.com/ArmisSecurity/armis-cli/internal/progress/progress.go:104:		NewSpinnerWithTimeout			100.0%
github.com/ArmisSecurity/armis-cli/internal/progress/progress.go:120:		NewSpinnerWithContext			100.0%
github.com/ArmisSecurity/armis-cli/internal/progress/progress.go:128:		SetWriter				100.0%
github.com/ArmisSecurity/armis-cli/internal/progress/progress.go:137:		Start					93.8%
github.com/ArmisSecurity/armis-cli/internal/progress/progress.go:225:		Stop					100.0%
github.com/ArmisSecurity/armis-cli/internal/progress/progress.go:260:		UpdateMessage				100.0%
github.com/ArmisSecurity/armis-cli/internal/progress/progress.go:267:		Update					100.0%
github.com/ArmisSecurity/armis-cli/internal/progress/progress.go:274:		GetElapsed				100.0%
github.com/ArmisSecurity/armis-cli/internal/progress/progress.go:281:		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:43:		NewScanner				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:57:		WithPollInterval			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:63:		WithSBOMVEXOptions			0.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:69:		ScanImage				0.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:101:		ScanTarball				76.6%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:187:		exportImage				0.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:228:		isDockerAvailable			42.9%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:242:		getDockerCommand			75.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:251:		validateDockerCommand			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:258:		buildScanResult				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:285:		convertNormalizedFindings		87.9%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:401:		shouldFilterByExploitability		100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:420:		cleanDescription			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:439:		isEmptyFinding				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/validate.go:11:		validateImageName			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/ignore.go:18:		LoadIgnorePatterns			75.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/ignore.go:52:		loadIgnoreFile				89.5%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/ignore.go:86:		Match					100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/ignore.go:98:		shouldSkipDir				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:42:		NewScanner				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:56:		WithPollInterval			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:62:		WithIncludeFiles			0.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:68:		WithSBOMVEXOptions			0.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:74:		Scan					70.9%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:235:		tarGzDirectory				71.8%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:315:		isPathContained				75.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:324:		tarGzFiles				78.6%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:410:		calculateFilesSize			0.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:431:		calculateDirSize			81.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:470:		shouldSkip				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:501:		isTestFile				88.9%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:544:		buildScanResult				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:571:		convertNormalizedFindings		76.8%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:686:		shouldFilterByExploitability		100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:705:		cleanDescription			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:726:		generateFindingTitle			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:780:		isEmptyFinding				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/sbom_vex.go:37:		NewSBOMVEXDownloader			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/sbom_vex.go:49:		Download				85.2%
github.com/ArmisSecurity/armis-cli/internal/scan/sbom_vex.go:101:		downloadAndSave				76.5%
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/update/update.go:65:		NewChecker				100.0%
github.com/ArmisSecurity/armis-cli/internal/update/update.go:80:		CheckInBackground			100.0%
github.com/ArmisSecurity/armis-cli/internal/update/update.go:100:		check					85.7%
github.com/ArmisSecurity/armis-cli/internal/update/update.go:143:		fetchLatestVersion			89.5%
github.com/ArmisSecurity/armis-cli/internal/update/update.go:176:		getCacheFilePath			44.4%
github.com/ArmisSecurity/armis-cli/internal/update/update.go:194:		readCache				84.6%
github.com/ArmisSecurity/armis-cli/internal/update/update.go:217:		writeCache				76.9%
github.com/ArmisSecurity/armis-cli/internal/update/update.go:240:		IsNewer					100.0%
github.com/ArmisSecurity/armis-cli/internal/update/update.go:263:		parseVersion				100.0%
github.com/ArmisSecurity/armis-cli/internal/update/update.go:284:		FormatNotification			100.0%
github.com/ArmisSecurity/armis-cli/internal/update/update.go:301:		getUpdateCommand			40.0%
github.com/ArmisSecurity/armis-cli/internal/util/format.go:7:			FormatCategory				100.0%
github.com/ArmisSecurity/armis-cli/internal/util/mask.go:54:			MaskSecretInLine			81.2%
github.com/ArmisSecurity/armis-cli/internal/util/mask.go:93:			maskValue				83.3%
github.com/ArmisSecurity/armis-cli/internal/util/mask.go:119:			MaskSecretInLines			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)				79.9%

Copy link

Copilot AI left a 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 improves armis-cli usability by adding centralized color handling for stderr output and introducing a background version update check against GitHub releases, plus spinner/status messaging improvements during scans.

Changes:

  • Add internal/cli color utilities (--color flag, NO_COLOR/TTY detection) and migrate warnings/errors to use them.
  • Add internal/update with async GitHub “latest release” checking + on-disk caching, and print update notifications after scan commands.
  • Enhance scan progress UX by updating spinner messages based on ingest status callbacks.

Reviewed changes

Copilot reviewed 22 out of 23 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
internal/update/update.go Implements GitHub release polling, TTL cache, and notification formatting.
internal/update/update_test.go Adds unit tests for version parsing/comparison, caching, network/error paths, and async checks.
internal/cli/color.go Adds centralized color enablement logic + PrintWarning/Error helpers.
internal/cli/color_test.go Tests color mode precedence and colored/plain stderr output.
internal/cmd/root.go Adds --color/--no-update-check, initializes color state, and starts update checks in the background.
internal/cmd/scan_repo.go Adds command examples and prints update notification before exiting.
internal/cmd/scan_image.go Adds command examples and prints update notification before exiting.
internal/cmd/context.go Routes cancellation messaging through the new warning printer.
internal/api/client.go Extends WaitForIngest with an optional per-poll status callback.
internal/api/client_test.go Updates WaitForIngest call sites and adds callback coverage.
internal/scan/repo/repo.go Uses cli warnings and updates spinner text via ingest status callback.
internal/scan/repo/repo_test.go Adds tests for new repo scan status formatting helper.
internal/scan/image/image.go Uses cli warnings and updates spinner text via ingest status callback.
internal/scan/image/helpers_test.go Adds tests for new image scan status formatting helper.
internal/scan/sbom_vex.go Migrates SBOM/VEX warning output to cli printers.
internal/output/human.go Adds SyncColors() to align formatter colors with centralized CLI color state.
internal/output/output.go Clarifies stderr writer usage for testability during stdout flush failures.
internal/output/output_test.go Adds tests to verify output.SyncColors() behavior.
internal/output/sarif.go Routes SARIF sanitization warnings through cli printers.
cmd/armis-cli/main.go Initializes colors early and prints top-level errors via cli.PrintError.
go.mod Adds golang.org/x/term as a direct dependency for TTY detection.
.goreleaser.yaml Generates shell completions during release builds and includes them in archives.
.gitignore Ignores dist/ and generated completions/.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

}

// readCache attempts to read a cached check result.
// Returns nil if cache is missing, corrupt, or expired.
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

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

The comment says readCache returns nil if the cache is "expired", but this function doesn’t check TTL/expiry (expiry is handled in Checker.check). Please update the comment to avoid misleading future readers (e.g., say missing/corrupt only).

Suggested change
// Returns nil if cache is missing, corrupt, or expired.
// Returns nil if cache is missing or corrupt.

Copilot uses AI. Check for mistakes.
Comment on lines +118 to +122
// Write to cache (best-effort)
c.writeCache(&cacheFile{
LatestVersion: latest,
CheckedAt: time.Now(),
})
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

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

Checker.check writes to the cache even when fetchLatestVersion returns an empty tag. Caching an empty LatestVersion can suppress future update checks for the full TTL; consider treating empty tag as an error (skip caching and return nil) so the next run can retry.

Copilot uses AI. Check for mistakes.
Comment on lines 67 to 75
// Skip update check if:
// - explicitly disabled via flag or env var
// - running in CI
// - version is "dev" (development build)
// - running meta-commands
if noUpdateCheck || os.Getenv("ARMIS_NO_UPDATE_CHECK") != "" ||
progress.IsCI() || version == "dev" ||
cmd.Name() == "help" || cmd.Name() == "__complete" {
return nil
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

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

Update checks are only skipped for help and __complete, but Cobra’s built-in "completion" command (used by the goreleaser hooks) will still start a GitHub update check. Consider also skipping update checks for cmd.Name()=="completion" (and possibly other meta commands like "version") to avoid unnecessary network calls/rate limits during completion generation.

Copilot uses AI. Check for mistakes.
var buf bytes.Buffer
if _, err := buf.ReadFrom(r); err != nil {
t.Errorf("failed to read from pipe: %v", err)
}
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

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

captureStderr closes the write end of the pipe but never closes the read end (r). Please close r as well to avoid leaking file descriptors across the test suite.

Suggested change
}
}
if err := r.Close(); err != nil {
t.Errorf("failed to close pipe reader: %v", err)
}

Copilot uses AI. Check for mistakes.
Comment on lines 119 to 130
func contains(s, substr string) bool {
return len(s) >= len(substr) && (s == substr || len(s) > 0 && containsHelper(s, substr))
}

func containsHelper(s, substr string) bool {
for i := 0; i <= len(s)-len(substr); i++ {
if s[i:i+len(substr)] == substr {
return true
}
}
return false
}
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

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

This test file reimplements substring search via contains/containsHelper. Using strings.Contains would simplify the test and remove custom logic that’s easy to get subtly wrong (especially around edge cases like empty substrings).

Copilot uses AI. Check for mistakes.
Comment on lines 818 to 825
// formatScanStatus returns a human-readable message for the current scan phase.
// Status values from ArtifactScanStatus enum in Project-Moose API.
func formatScanStatus(scanStatus string) string {
switch strings.ToUpper(scanStatus) {
case "INITIATED":
return "Scan initiated, preparing analysis..."
case "IN_PROGRESS":
return "Analyzing code for vulnerabilities..."
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

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

formatScanStatus is duplicated (repo and image scanners) with only minor message differences. Consider extracting a shared helper (e.g., internal/scan/status.go) to avoid the two copies drifting as new statuses are added.

Copilot uses AI. Check for mistakes.
Comment on lines 480 to 493
switch strings.ToUpper(scanStatus) {
case "INITIATED":
return "Scan initiated, preparing analysis..."
case "IN_PROGRESS":
return "Scanning image for vulnerabilities..."
case "COMPLETED":
return "Scan completed, preparing results..."
case "FAILED":
return "Scan encountered an error"
case "STOPPED":
return "Scan was stopped"
default:
return fmt.Sprintf("Scanning... [%s]", strings.ToUpper(scanStatus))
}
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

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

formatScanStatus is duplicated (repo and image scanners) with only minor message differences. Consider extracting a shared helper (e.g., internal/scan/status.go) to avoid the two copies drifting as new statuses are added.

Suggested change
switch strings.ToUpper(scanStatus) {
case "INITIATED":
return "Scan initiated, preparing analysis..."
case "IN_PROGRESS":
return "Scanning image for vulnerabilities..."
case "COMPLETED":
return "Scan completed, preparing results..."
case "FAILED":
return "Scan encountered an error"
case "STOPPED":
return "Scan was stopped"
default:
return fmt.Sprintf("Scanning... [%s]", strings.ToUpper(scanStatus))
}
statusKey := strings.ToUpper(scanStatus)
statusMessages := map[string]string{
"INITIATED": "Scan initiated, preparing analysis...",
"IN_PROGRESS": "Scanning image for vulnerabilities...",
"COMPLETED": "Scan completed, preparing results...",
"FAILED": "Scan encountered an error",
"STOPPED": "Scan was stopped",
}
if msg, ok := statusMessages[statusKey]; ok {
return msg
}
return fmt.Sprintf("Scanning... [%s]", statusKey)

Copilot uses AI. Check for mistakes.
Security fixes:
- CWE-73: Add path validation in update.go using util.SanitizePath()
- CWE-770: Add io.LimitReader to all io.ReadAll calls in api/client.go

Code quality improvements:
- Skip update checks for completion commands and subcommands
- Don't cache empty version tags from GitHub API
- Fix misleading readCache comment (removed "expired" mention)

Test fixes:
- Close pipe reader in color_test.go to prevent FD leak
- Replace custom contains() helper with strings.Contains in update_test.go

Refactoring:
- Extract shared scan helpers (FormatScanStatus, FormatElapsed, MapSeverity)
  to internal/scan/status.go to eliminate code duplication between
  repo and image scanners
Copilot AI review requested due to automatic review settings February 12, 2026 07:53
Copy link

Copilot AI left a 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 26 out of 27 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 84 to 86
with:
version: v2.7.2
version: latest
args: --timeout=5m
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

Using golangci-lint version latest makes CI non-deterministic and can break builds unexpectedly when new linter releases add checks or change defaults. Please pin this to a specific known-good golangci-lint version (and update it intentionally when desired).

Copilot uses AI. Check for mistakes.
CWE-770: Replace json.NewDecoder with io.LimitReader + json.Unmarshal
- StartIngest: limit IngestUploadResponse decoding
- GetIngestStatus: limit IngestStatusResponse decoding
- GetScanResult: limit ScanResult decoding

CWE-73: Add inline path validation in update.go
- readCache: validate path with util.SanitizePath before os.ReadFile
- writeCache: validate path with util.SanitizePath before os.WriteFile

CI: Pin golangci-lint to v2.1.6 for reproducible builds
_ = ctx // unused but kept for API consistency
if errors.Is(err, context.Canceled) {
fmt.Fprintln(os.Stderr, "\nScan cancelled")
fmt.Fprintln(os.Stderr, "") // newline before warning

Check warning

Code scanning / Armis Security Scanner

The return value (error) from fmt.Fprintln is ignored, so failures when writi... Medium

The return value (error) from fmt.Fprintln is ignored, so failures when writi...: The return value (error) from fmt.Fprintln is ignored, so failures when writing to standard error are not detected.
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