-
Notifications
You must be signed in to change notification settings - Fork 1
[PPSC-402] feat(cli): improve CLI usability with color support and update checks #69
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
base: main
Are you sure you want to change the base?
Conversation
…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
🛡️ Armis Security Scan Results🟠 HIGH issues found
Total: 5 View all 5 findings🟠 HIGH (4)
|
Test Coverage Reporttotal: (statements) 79.9% Coverage by function |
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 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/clicolor utilities (--colorflag, NO_COLOR/TTY detection) and migrate warnings/errors to use them. - Add
internal/updatewith 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.
internal/update/update.go
Outdated
| } | ||
|
|
||
| // readCache attempts to read a cached check result. | ||
| // Returns nil if cache is missing, corrupt, or expired. |
Copilot
AI
Feb 11, 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 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).
| // Returns nil if cache is missing, corrupt, or expired. | |
| // Returns nil if cache is missing or corrupt. |
| // Write to cache (best-effort) | ||
| c.writeCache(&cacheFile{ | ||
| LatestVersion: latest, | ||
| CheckedAt: time.Now(), | ||
| }) |
Copilot
AI
Feb 11, 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.
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.
| // 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 |
Copilot
AI
Feb 11, 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.
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.
| var buf bytes.Buffer | ||
| if _, err := buf.ReadFrom(r); err != nil { | ||
| t.Errorf("failed to read from pipe: %v", err) | ||
| } |
Copilot
AI
Feb 11, 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.
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.
| } | |
| } | |
| if err := r.Close(); err != nil { | |
| t.Errorf("failed to close pipe reader: %v", err) | |
| } |
internal/update/update_test.go
Outdated
| 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 | ||
| } |
Copilot
AI
Feb 11, 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.
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).
internal/scan/repo/repo.go
Outdated
| // 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..." |
Copilot
AI
Feb 11, 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.
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.
internal/scan/image/image.go
Outdated
| 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)) | ||
| } |
Copilot
AI
Feb 11, 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.
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.
| 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) |
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
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 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.
| with: | ||
| version: v2.7.2 | ||
| version: latest | ||
| args: --timeout=5m |
Copilot
AI
Feb 12, 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.
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).
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
Related Issue
Type of Change
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:
Color Output Management (
internal/cli/color.go)Version Update Checking (
internal/update/update.go)Additional improvements:
Testing
Automated Tests
Manual Testing
Verified by running:
make test- All tests passmake lint- No linting errorsarmis-cli --help,armis-cli --color=never, etc.Reviewer Notes
golang.org/x/termfor cross-platform TTY detectionChecklist