Skip to content

Conversation

@yiftach-armis
Copy link
Collaborator

Description

This PR addresses critical security vulnerabilities identified during an Armis scan, including path traversal attacks, goroutine leaks, credential exposure, and improper resource management. The changes implement defense-in-depth mitigations across input validation, path handling, progress tracking, and output formatting.

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • Security fix (addresses CWE-22, resource leaks, credential exposure)

Changes Summary

Path Traversal Prevention

  • New SafeJoinPath utility that validates path joining stays within base directory
  • Enhanced SanitizePath with early detection of ".." traversal patterns
  • Applied to human formatter file snippet loading and git blame operations
  • Multipart upload filename sanitization using filepath.Base

Goroutine Leak Prevention

  • Spinner now supports context cancellation via NewSpinnerWithContext
  • Added automatic timeout safety net (30 min default) to prevent indefinite goroutines
  • Idempotent Stop() using sync.Once - safe to call multiple times
  • Proper context lifecycle management with defer cleanup

Secret Masking

  • New util/mask.go with regex patterns for detecting common secrets (API keys, passwords, AWS credentials, JWT tokens)
  • Secrets in code snippets are masked while preserving code structure
  • Applied to both image and repo scan findings when HasSecret is detected

Input Validation & Exit Codes

  • New validateFailOn() for severity level validation (INFO, LOW, MEDIUM, HIGH, CRITICAL)
  • Exit code normalization to POSIX range (0-255)
  • Install script validation: version format and install directory path

Resource Management Improvements

  • HTTP client closes response body on error to prevent leaks
  • Better error handling when reading response body
  • File close error propagation in human formatter

Testing

  • 261+ new unit tests for spinner behavior (idempotency, context cancellation, timeout, goroutine leak detection)
  • 176+ tests for path utilities (SanitizePath, SafeJoinPath)
  • 152+ tests for secret masking with various patterns
  • 78+ tests for severity level validation
  • All existing tests pass

Checklist

  • Code follows project style and conventions
  • Self-reviewed for correctness and security
  • Comments explain security rationale where necessary
  • No new warnings introduced
  • Comprehensive test coverage added
  • All unit tests pass locally

Security Improvements

  • CWE-22 (Path Traversal): Fixed in file path handling
  • Resource Leaks: Goroutine leak prevention with context support
  • Credential Exposure: Sensitive data masking in output
  • Command Injection: Input validation in install scripts

@github-actions
Copy link

github-actions bot commented Jan 11, 2026

Test Coverage Report

total: (statements) 76.8%

Coverage by function
github.com/ArmisSecurity/armis-cli/cmd/armis-cli/main.go:16:			main					0.0%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:36:			WithHTTPClient				100.0%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:44:			NewClient				91.7%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:88:			IsDebug					100.0%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:93:			StartIngest				78.4%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:160:			GetIngestStatus				84.2%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:193:			WaitForIngest				0.0%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:235:			FetchNormalizedResults			78.6%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:282:			FetchAllNormalizedResults		91.7%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:307:			GetScanResult				66.7%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:335:			WaitForScan				0.0%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:356:			formatBytes				100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:41:			SetVersion				100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:49:			Execute					100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:53:			init					100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:65:			getEnvOrDefault				100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:72:			getEnvOrDefaultInt			100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:82:			getAPIBaseURL				100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:89:			getToken				100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:96:			getTenantID				100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:103:			getPageLimit				100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:110:			validatePageLimit			100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:120:			validateFailOn				100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:134:			getFailOn				100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/scan.go:21:			init					100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/scan_image.go:96:		init					100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/scan_repo.go:74:		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					87.5%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:31:			write					66.7%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:62:			Write					90.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:93:			Format					100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:98:			FormatWithOptions			96.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:136:		getSeverityIcon				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:153:		getSeverityColor			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:182:		init					50.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:189:		disableColors				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:201:		sortFindingsBySeverity			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:230:		loadSnippetFromFile			75.5%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:312:		formatCodeSnippet			0.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:349:		highlightColumns			0.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:385:		detectLanguage				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:683:		scanDuration				26.3%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:715:		renderSummaryDashboard			61.2%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:804:		renderFindings				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:819:		renderFinding				62.5%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:876:		renderGroupedFindings			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:897:		groupFindings				96.6%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:952:		severityRank				75.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:966:		isGitRepo				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:973:		getGitBlame				0.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1009:		parseGitBlame				85.7%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1045:		maskEmail				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1068:		getTopLevelDomain			75.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:110:		FormatWithOptions			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/output.go:25:		GetFormatter				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/output.go:41:		ShouldFail				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/output.go:57:		ExitIfNeeded				0.0%
github.com/ArmisSecurity/armis-cli/internal/output/sarif.go:64:			Format					100.0%
github.com/ArmisSecurity/armis-cli/internal/output/sarif.go:87:			convertToSarifResults			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/sarif.go:124:		severityToSarifLevel			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/sarif.go:138:		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					89.6%
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/image/image.go:40:		NewScanner				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:54:		WithPollInterval			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:60:		ScanImage				0.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:93:		ScanTarball				93.1%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:141:		exportImage				0.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:174:		isDockerAvailable			42.9%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:188:		getDockerCommand			75.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:197:		validateDockerCommand			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:204:		buildScanResult				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:231:		convertNormalizedFindings		93.9%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:325:		shouldFilterByExploitability		100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:344:		cleanDescription			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:363:		isEmptyFinding				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:376:		mapSeverity				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:391:		formatElapsed				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/validate.go:11:		validateImageName			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:38:		NewScanner				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:52:		WithPollInterval			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:58:		Scan					84.6%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:145:		tarGzDirectory				69.2%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:217:		calculateDirSize			78.9%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:250:		shouldSkip				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:281:		isTestFile				88.9%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:324:		buildScanResult				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:351:		convertNormalizedFindings		90.2%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:449:		shouldFilterByExploitability		100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:468:		cleanDescription			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:487:		isEmptyFinding				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:500:		mapSeverity				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:515:		formatElapsed				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/util/mask.go:43:			MaskSecretInLine			81.2%
github.com/ArmisSecurity/armis-cli/internal/util/mask.go:79:			maskValue				60.0%
github.com/ArmisSecurity/armis-cli/internal/util/mask.go:94:			MaskSecretInLines			100.0%
github.com/ArmisSecurity/armis-cli/internal/util/path.go:12:			SanitizePath				90.9%
github.com/ArmisSecurity/armis-cli/internal/util/path.go:41:			SafeJoinPath				84.2%
github.com/ArmisSecurity/armis-cli/test/sample-repo/src/main.go:6:		main					0.0%
total:										(statements)				76.8%

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 implements comprehensive security hardening to address vulnerabilities identified by an Armis security scan. The changes focus on preventing path traversal attacks (CWE-22), goroutine leaks, credential exposure, and improving resource management through defense-in-depth mitigations across input validation, path handling, progress tracking, and output formatting.

Changes:

  • Implemented path traversal prevention with new SafeJoinPath utility and enhanced SanitizePath validation, applied across file operations and install scripts
  • Added goroutine leak prevention through context-aware spinners with timeout safety nets and idempotent stop operations
  • Introduced secret masking functionality to redact sensitive values in code snippets when secrets are detected
  • Enhanced input validation for severity levels and exit codes, with normalized POSIX-compliant exit code ranges
  • Improved resource management with proper HTTP response body cleanup and file close error propagation

Reviewed changes

Copilot reviewed 19 out of 19 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
scripts/install.sh Added version and install directory validation to prevent command injection and path traversal
scripts/install.ps1 Added path traversal validation for install directory
internal/util/path.go New SafeJoinPath function and enhanced SanitizePath for path traversal prevention
internal/util/path_test.go Comprehensive test coverage for path validation (176+ tests)
internal/util/mask.go New secret masking utility with regex patterns for common credential formats
internal/util/mask_test.go Test coverage for secret masking functionality (152+ tests)
internal/progress/progress.go Context-aware spinners with timeout, idempotent Stop(), and goroutine leak prevention
internal/progress/progress_test.go Extensive spinner behavior tests including leak detection (261+ tests)
internal/scan/repo/repo.go Applied path validation and context-aware spinners with proper defer cleanup
internal/scan/image/image.go Applied secret masking and context-aware spinners
internal/output/human.go Applied SafeJoinPath for file snippet loading and git blame operations
internal/output/output.go Exit code normalization to POSIX range (0-255)
internal/output/json.go Debug mode support with format options metadata
internal/httpclient/client.go HTTP response body cleanup on error to prevent resource leaks
internal/api/client.go Filename sanitization using filepath.Base for multipart uploads, non-HTTPS warning
internal/cmd/root.go Severity level validation with validateFailOn()
internal/cmd/root_test.go Test coverage for severity validation (78+ tests)
internal/cmd/scan_repo.go Integrated severity validation with error handling
internal/cmd/scan_image.go Integrated severity validation with error handling

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

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 20 out of 20 changed files in this pull request and generated 5 comments.


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

@yiftach-armis yiftach-armis requested a review from Copilot January 12, 2026 10:43
yiftach-armis and others added 4 commits January 12, 2026 12:43
- Improve path sanitization in util/path.go to detect traversal
  patterns before filepath.Clean() normalizes them
- Fix HTTP response body leak in httpclient/client.go when
  requests fail
- Sanitize filename in multipart upload using filepath.Base()
- Add HTTPS warning for non-localhost URLs in API client
- Validate InstallDir in PowerShell install script
- Add secret masking utility to redact sensitive values in
  code snippets when HasSecret=true
- Fix JWT regex to correctly detect tokens (header starts with
  eyJ, but payload does not necessarily)

Co-Authored-By: Claude <noreply@anthropic.com>
- Reject absolute paths without repository context in loadSnippetFromFile
- Add immutability comments for thread-safe Spinner fields
- Reduce false positives in credential masking with word boundaries and 8-char min
- Remove overly strict consecutive slash check from install script

Co-Authored-By: Claude <noreply@anthropic.com>
- Reorder secret patterns from most specific to least specific to prevent
  early matches (e.g., AWS credentials before generic "secret" pattern)
- Use conventional hyphen placement [-_] in regex character classes
- Fix SanitizePath to check ".." as path component instead of substring,
  allowing valid filenames like "my..file.txt"
- Recreate spinner channels on Start() for robustness against future reuse
- Simplify PowerShell path traversal check for clarity

Co-Authored-By: Claude <noreply@anthropic.com>
- Add documentation explaining SafeJoinPath early check is optimization,
  final filepath.Rel verification is authoritative (path.go)
- Use path segment check instead of substring match in install.sh to
  allow valid paths like /opt/app..v1/bin while rejecting traversal
- Reset stopOnce along with channels in spinner Start() for consistency
- Increase minimum lengths in mask patterns to reduce false positives:
  private keys 10→16, API keys 8→10, passwords 4→8, generic creds 8→10
- Wrap both errors in defer if scanner and close errors both occur (human.go)

Co-Authored-By: Claude <noreply@anthropic.com>
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 20 out of 20 changed files in this pull request and generated 1 comment.


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

yiftach-armis and others added 2 commits January 12, 2026 13:03
- Add commonLiterals filter to mask.go to skip masking common
  programming literals (true, false, null, nil, undefined, none, empty)
- Add version format validation to install.ps1 matching the bash script

Co-Authored-By: Claude <noreply@anthropic.com>
- Add ANSI clear-to-end-of-line escape sequence (\033[K) on every
  spinner frame update to prevent character leaking when output
  length changes
- Add explicit spinner.Stop() calls before fmt.Printf statements
  to prevent race condition between spinner goroutine and main
  output
- Remove leading newline from Printf and move spacing to end to
  prevent visual flicker during spinner transitions

Co-Authored-By: Claude <noreply@anthropic.com>
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 20 out of 20 changed files in this pull request and generated 2 comments.


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

Address Copilot code review comments:
- Capture startTime and message in local variables while holding mutex
  before using them in CI path and goroutine
- Set s.cancel under mutex protection after context creation
- Read s.cancel under mutex in Stop() to avoid race with Start()
- Protect GetElapsed() read of startTime with mutex

All tests pass with -race flag enabled.

Co-Authored-By: Claude <noreply@anthropic.com>
@yiftach-armis yiftach-armis merged commit 6701446 into main Jan 12, 2026
6 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.

2 participants