Skip to content

[PPSC-563] feat(auth): remove auth-endpoint flag#98

Open
yiftach-armis wants to merge 8 commits intomainfrom
feat/PPSC-563-remove-auth-endpoint
Open

[PPSC-563] feat(auth): remove auth-endpoint flag#98
yiftach-armis wants to merge 8 commits intomainfrom
feat/PPSC-563-remove-auth-endpoint

Conversation

@yiftach-armis
Copy link
Collaborator

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

Related Issue

Type of Change

  • New feature (non-breaking change which adds functionality)
  • Documentation update
  • Refactoring (no functional changes)

Problem

The --auth-endpoint flag required users to manually specify the JWT authentication service endpoint, creating unnecessary configuration burden and potential attack surface. Auth credentials now route through moose's unified proxy endpoint automatically.

Solution

  • Replaced AuthEndpoint config with BaseURL derived from --dev flag or production defaults
  • Updated auth client to call /api/v1/auth/token (moose) instead of /api/v1/authenticate (Silk)
  • Removed --auth-endpoint flag and ARMIS_AUTH_ENDPOINT environment variable
  • Added region claim support to JWT for future deployment-aware routing
  • Clarified internal naming (endpointbaseURL in NewAuthClient)
  • Updated documentation and error messages

Testing

  • Unit tests added/updated
  • All tests passing locally
  • Code follows project style guidelines
  • Documentation updated

Reviewer Notes

This change fixes findings from deep-review analysis:

  1. Removed stale documentation references to removed env var
  2. Clarified NewAuthClient naming to match semantic change from endpoint to base URL

Includes region support groundwork for PPSC-543.

Simplify CLI authentication by removing the --auth-endpoint flag entirely.
Auth now routes through moose's proxy endpoint automatically, with the base
URL derived from the --dev flag or hardcoded production endpoint.

Changes:
- Replace AuthEndpoint config with BaseURL (derived from getAPIBaseURL)
- Update auth client endpoint from /api/v1/authenticate to /api/v1/auth/token
- Remove --auth-endpoint flag and ARMIS_AUTH_ENDPOINT environment variable
- Add region claim support to JWT parsing for deployment-aware routing
- Rename endpoint → baseURL in NewAuthClient for semantic clarity
- Update error messages to reflect base URL terminology
- Update all tests and documentation

This simplifies the auth flow by removing a user-configurable endpoint,
reducing attack surface and improving UX when configuring authentication.
Copilot AI review requested due to automatic review settings March 11, 2026 10:10
@github-actions
Copy link

github-actions bot commented Mar 11, 2026

Armis AppSecArmis AppSec Security Scan Results

🟠 HIGH issues found

Severity Count
🟠 HIGH 7

Total: 7

View all 7 findings

🟠 HIGH (7)

CWE-522_armis-cli_38295677_internal/cmd/auth.go_62_2_62_19 - Insecure Design (CWE-522

Location: internal/cmd/auth.go:62

Insufficiently Protected Credentials): The runAuth function obtains a JWT token using the provided client credentials and then prints the raw token directly to standard output with fmt.Println(token). This output can be captured by other processes, logged, or appear in command histories, exposing the credential. The token originates from the client secret (a sensitive source) and reaches the stdout sink without any sanitization, making the credential reachable. The command-line interface represents script-to-script interaction, giving it an exposure level of 5, which yields a low likelihood score (5). This matches CWE-522: Insufficiently Protected Credentials.

Code snippet is redacted as it contains secrets.

CWEs: CWE-522: Insufficiently Protected Credentials

CWE-522_armis-cli_38295677_internal/cmd/root.go_159_2_159_31 - Insecure Design (CWE-522

Location: internal/cmd/root.go:159

Insufficiently Protected Credentials): The init function registers a --token (or -t) command‑line flag that stores an API token in the variable token. Because the token can be supplied directly on the command line, it becomes part of the process's argument list, which is visible to other users on the same system via typical process‑listing tools. The value from this flag (or its default environment variable) is later passed unchanged to the authentication provider in getAuthProvider, where it is used for API calls. No sanitization or masking occurs before the token is used. Consequently, an attacker who can view process arguments can obtain the credential, satisfying CWE‑522 (Insufficiently Protected Credentials). The vulnerability is exposed through a user‑facing CLI interface, making it immediately reachable to external actors.

Code snippet is redacted as it contains secrets.

CWEs: CWE-522: Insufficiently Protected Credentials

CWE-522_armis-cli_38295677_internal/cmd/root.go_165_2_165_169 - Insecure Design (CWE-522

Location: internal/cmd/root.go:165

Insufficiently Protected Credentials): The init function registers a persistent CLI flag --client-secret that stores the provided value in the clientSecret variable. Because this flag is supplied on the command line, the secret becomes part of the process's argument list, which is visible to any user who can list processes on the host (e.g., via ps). The code does not apply any protection, masking, or sanitization before the value is later passed to the authentication provider. Consequently, an attacker with local system access can read the command‑line arguments and obtain the client secret, enabling unauthorized use of the JWT authentication flow. This matches CWE‑522 (Insufficiently Protected Credentials) and is exploitable through ordinary process‑listing techniques. The exposure level is classified as 5 (script‑to‑script interaction) because the credential can be harvested by other processes on the same machine.

Code snippet is redacted as it contains secrets.

CWEs: CWE-522: Insufficiently Protected Credentials

CWE-319_armis-cli_38295677_internal/auth/client.go_101_2_101_124 - Cryptography Failures (CWE-319

Location: internal/auth/client.go:101

Cleartext Transmission of Sensitive Information): The Authenticate method builds a JSON body that includes the clientSecret argument and sends it with an HTTP POST request using c.httpClient.Do(req). Because the client permits the use of the HTTP scheme for localhost URLs, the request may travel over an unencrypted connection. Consequently, the secret credential is transmitted in cleartext to the authentication endpoint, exposing it to any local network observer. No sanitization or encryption is applied to the secret before transmission, making the flow from source (function argument) to sink (network request) reachable. This constitutes a cleartext transmission of sensitive information, matching CWE‑319.

Code snippet is redacted as it contains secrets.

CWEs: CWE-319: Cleartext Transmission of Sensitive Information

CWE-532_armis-cli_38295677_internal/auth/client.go_120_4_120_105 - Security Logging and Monitoring Failures (CWE-532

Location: internal/auth/client.go:120

Information Exposure Through Log Files): The Authenticate method reads the HTTP response body from an external authentication service. When the request fails (non‑200 status) and the client is configured with debug mode enabled, the code writes the full response body to standard error via fmt.Fprintf(os.Stderr, ...). Because the response body originates from an external source, it can contain sensitive information such as error details, internal identifiers, or even tokens. Writing this data to stderr may cause it to be captured in application logs, which could be accessed by other processes or personnel, leading to unintended information disclosure. No sanitization is performed before logging, so the tainted data reaches the log sink directly. This matches CWE‑532: Information Exposure Through Log Files, making the finding a true positive.

CWEs: CWE-532: Information Exposure Through Log Files

CWE-522_armis-cli_38295677_internal/auth/auth.go_179_2_179_32 - Insecure Design (CWE-522

Location: internal/auth/auth.go:179

Insufficiently Protected Credentials): The GetRawToken method (lines 167‑180) returns the JWT or legacy token exactly as stored in p.credentials.Token (or p.config.Token for legacy mode). No masking, encryption, or additional protection is applied before the token is handed to the caller. Because this method is used by the CLI to print the token to stdout, an attacker who can invoke the CLI (or any code that calls this method) can obtain the raw authentication credential and reuse it to impersonate the user against the API. The token originates from the authentication exchange and is propagated unchanged to the return value, with no sanitizers present. This constitutes an instance of CWE‑522: Insufficiently Protected Credentials. The functionality is reachable from a command‑line interface, which is classified as script‑to‑script interaction, giving it an exposure rating of 5 and a corresponding low likelihood score.

Code snippet is redacted as it contains secrets.

CWEs: CWE-522: Insufficiently Protected Credentials

CWE-522_armis-cli_38295677_internal/auth/auth.go_169_3_169_28 - Insecure Design (CWE-522

Location: internal/auth/auth.go:169

Insufficiently Protected Credentials): The GetRawToken method returns the authentication token in plain form, without any protection or masking. This directly exposes credentials to any caller that can invoke the method, which matches CWE‑522 (Insufficiently Protected Credentials). The function is part of a CLI tool that can be triggered by a user, making the token reachable through a user‑facing interface. No sanitization is applied before the token is returned, so the credential flow is reachable. Consequently, the vulnerability is a true positive with high severity and a high likelihood of exploitation due to the immediate external exposure.

Code snippet is redacted as it contains secrets.

CWEs: CWE-522: Insufficiently Protected Credentials

@github-actions
Copy link

github-actions bot commented Mar 11, 2026

Test Coverage Report

total: (statements) 80.7%

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:61:			copyWithContext				70.4%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:134:			WithHTTPClient				100.0%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:143:			WithUploadHTTPClient			100.0%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:151:			WithAllowLocalURLs			100.0%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:163:			NewClient				100.0%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:211:			IsDebug					100.0%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:225:			setAuthHeader				77.8%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:259:			StartIngest				72.3%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:418:			GetIngestStatus				82.6%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:459:			WaitForIngest				84.6%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:510:			FetchNormalizedResults			74.2%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:565:			FetchAllNormalizedResults		91.7%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:590:			GetScanResult				68.4%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:625:			WaitForScan				90.0%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:646:			formatBytes				100.0%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:668:			FetchArtifactScanResults		75.0%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:723:			ValidatePresignedURL			100.0%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:759:			DownloadFromPresignedURL		84.2%
github.com/ArmisSecurity/armis-cli/internal/auth/auth.go:56:			NewAuthProvider				95.2%
github.com/ArmisSecurity/armis-cli/internal/auth/auth.go:102:			GetAuthorizationHeader			100.0%
github.com/ArmisSecurity/armis-cli/internal/auth/auth.go:122:			GetTenantID				85.7%
github.com/ArmisSecurity/armis-cli/internal/auth/auth.go:139:			GetRegion				85.7%
github.com/ArmisSecurity/armis-cli/internal/auth/auth.go:154:			IsLegacy				100.0%
github.com/ArmisSecurity/armis-cli/internal/auth/auth.go:167:			GetRawToken				85.7%
github.com/ArmisSecurity/armis-cli/internal/auth/auth.go:194:			exchangeCredentials			87.5%
github.com/ArmisSecurity/armis-cli/internal/auth/auth.go:263:			refreshIfNeeded				100.0%
github.com/ArmisSecurity/armis-cli/internal/auth/auth.go:293:			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:81:			Authenticate				71.0%
github.com/ArmisSecurity/armis-cli/internal/auth/region_cache.go:30:		NewRegionCache				100.0%
github.com/ArmisSecurity/armis-cli/internal/auth/region_cache.go:36:		Load					91.7%
github.com/ArmisSecurity/armis-cli/internal/auth/region_cache.go:62:		Save					76.9%
github.com/ArmisSecurity/armis-cli/internal/auth/region_cache.go:92:		Clear					75.0%
github.com/ArmisSecurity/armis-cli/internal/auth/region_cache.go:102:		getFilePath				83.3%
github.com/ArmisSecurity/armis-cli/internal/auth/region_cache.go:119:		loadCachedRegion			100.0%
github.com/ArmisSecurity/armis-cli/internal/auth/region_cache.go:123:		saveCachedRegion			100.0%
github.com/ArmisSecurity/armis-cli/internal/auth/region_cache.go:127:		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:142:			SetVersion				100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:150:			Execute					100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:154:			init					100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:185:			PrintUpdateNotification			81.2%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:227:			printUpdateNotificationOnce		75.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:240:			getEnvOrDefault				100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:247:			getEnvOrDefaultInt			100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:257:			getAPIBaseURL				100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:270:			getAuthProvider				100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:282:			getPageLimit				100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:289:			validatePageLimit			100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:299:			validateFailOn				100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:317:			getFailOn				100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/scan.go:84:			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:44:		GetFormatter				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/output.go:60:		ShouldFail				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/output.go:78:		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:46:		NewScanner				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:60:		WithPollInterval			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:66:		WithSBOMVEXOptions			0.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:73:		WithPullPolicy				0.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:79:		ScanImage				0.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:110:		ScanTarball				77.1%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:201:		exportImage				0.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:251:		isDockerAvailable			42.9%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:265:		getDockerCommand			75.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:274:		validateDockerCommand			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:282:		imageExistsLocally			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:290:		determinePullBehavior			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:306:		buildScanResult				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:333:		convertNormalizedFindings		85.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:456:		shouldFilterByExploitability		100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:475:		cleanDescription			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:494:		isEmptyFinding				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:509:		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: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:43:		NewScanner				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:57:		WithPollInterval			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:63:		WithIncludeFiles			0.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:69:		WithSBOMVEXOptions			0.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:75:		Scan					70.9%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:240:		tarGzDirectory				71.8%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:323:		isPathContained				75.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:332:		tarGzFiles				78.6%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:419:		calculateFilesSize			0.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:440:		calculateDirSize			81.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:480:		shouldSkip				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:511:		isTestFile				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:555:		buildScanResult				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:582:		convertNormalizedFindings		73.3%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:705:		shouldFilterByExploitability		100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:724:		cleanDescription			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:745:		generateFindingTitle			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:749:		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.7%

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 updates JWT authentication to use the configured API base URL (Moose) instead of a separate --auth-endpoint, switches the token exchange route to /api/v1/auth/token, and extends JWT parsing to extract an optional region claim.

Changes:

  • Removed the --auth-endpoint / ARMIS_AUTH_ENDPOINT configuration path and wired JWT auth to use the API base URL (getAPIBaseURL() / ARMIS_API_URL override).
  • Updated the auth client to call POST /api/v1/auth/token and adjusted command/docs/tests accordingly.
  • Added region claim parsing + GetRegion() API on the auth provider, with tests for backward compatibility.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
internal/cmd/root.go Removes authEndpoint flag/var and passes API base URL into auth provider config.
internal/cmd/root_test.go Updates root auth-provider tests to reflect removal of authEndpoint.
internal/cmd/auth.go Removes --auth-endpoint requirement for the (hidden) auth command.
internal/cmd/auth_test.go Updates auth command tests to use ARMIS_API_URL and new route.
internal/auth/client.go Renames endpoint→baseURL and switches token exchange to /api/v1/auth/token.
internal/auth/auth.go Renames config field to BaseURL and adds region extraction + accessor.
internal/auth/auth_test.go Updates tests for BaseURL + new endpoint; adds coverage for region parsing/accessor.
docs/FEATURES.md Removes ARMIS_AUTH_ENDPOINT from documented env vars.
CLAUDE.md Updates internal docs to reflect the new token endpoint and removes ARMIS_AUTH_ENDPOINT.
Comments suppressed due to low confidence (1)

CLAUDE.md:90

  • This environment variable list no longer mentions ARMIS_AUTH_ENDPOINT, but it also doesn't mention ARMIS_API_URL, which the CLI uses as an override for the Moose/API base URL (and is now also used for JWT token exchange). Please add ARMIS_API_URL here so internal docs match the current configuration knobs.
### Environment Variables

- `ARMIS_CLIENT_ID` - Client ID for JWT authentication (recommended)
- `ARMIS_CLIENT_SECRET` - Client secret for JWT authentication
- `ARMIS_API_TOKEN` - API token for Basic authentication (fallback)
- `ARMIS_TENANT_ID` - Tenant identifier (required only with Basic auth; JWT extracts it from token)
- `ARMIS_FORMAT` - Default output format
- `ARMIS_PAGE_LIMIT` - Results pagination size
- `ARMIS_THEME` - Terminal background theme: auto, dark, light (default: auto)

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

Comment on lines +92 to +96
if origAPIURL == "" {
_ = os.Unsetenv("ARMIS_API_URL")
} else {
_ = os.Setenv("ARMIS_API_URL", origAPIURL)
}
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

In this test cleanup, os.Unsetenv/os.Setenv errors are ignored implicitly. With errcheck enabled (per repo lint config), bare calls like this are typically flagged. Please assign the returned error to _ (or handle it) to match patterns used elsewhere in internal/cmd tests and keep lint clean.

Copilot uses AI. Check for mistakes.
authEndpoint = server.URL
} else {
authEndpoint = ""
_ = os.Setenv("ARMIS_API_URL", server.URL)
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

os.Setenv returns an error; calling it without checking or assigning to _ is likely to be flagged by errcheck during CI. Please use _ = os.Setenv(...) (or assert no error) here.

Copilot uses AI. Check for mistakes.
Comment on lines 67 to 72
if config.ClientID != "" && config.ClientSecret != "" {
// JWT auth
// JWT auth via moose
p.isLegacy = false
if config.AuthEndpoint == "" {
return nil, fmt.Errorf("--auth-endpoint is required when using client credentials")
if config.BaseURL == "" {
return nil, fmt.Errorf("base URL is required for JWT authentication")
}
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

The comments around NewAuthProvider/AuthConfig still refer to using a specific "endpoint" for JWT auth, but the config field has been renamed to BaseURL and is now described as the Moose API base URL. Please update the nearby doc/comment text to consistently use “base URL” (or clarify the distinction if there still is one) so the public contract of this package remains accurate.

Copilot uses AI. Check for mistakes.
| `ARMIS_CLIENT_SECRET` | Client secret for JWT authentication |
| `ARMIS_AUTH_ENDPOINT` | Authentication service endpoint URL |
| `ARMIS_API_TOKEN` | API token for Basic authentication |
| `ARMIS_TENANT_ID` | Tenant identifier (required for Basic auth only) |
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

This env var table documents auth-related configuration, but it omits ARMIS_API_URL, which the CLI uses to override the API (and now JWT auth) base URL. Since ARMIS_AUTH_ENDPOINT is being removed here, consider adding ARMIS_API_URL to keep the docs aligned with the actual configuration surface.

Suggested change
| `ARMIS_TENANT_ID` | Tenant identifier (required for Basic auth only) |
| `ARMIS_TENANT_ID` | Tenant identifier (required for Basic auth only) |
| `ARMIS_API_URL` | Override base URL for Armis API and authentication (advanced) |

Copilot uses AI. Check for mistakes.
- Update NewAuthProvider comment to say "base URL" instead of "endpoint"
- Document ARMIS_API_URL in FEATURES.md and CLAUDE.md env var sections
Add region caching to optimize JWT authentication by persisting the
discovered region to disk and reusing it on subsequent CLI invocations.

Key changes:
- Add --region flag for explicit region override (bypasses auto-discovery)
- Implement file-based region cache (~/.cache/armis-cli/region-cache.json)
- Add automatic retry without region hint when cached region auth fails
- Memoize cached region in AuthProvider to avoid repeated disk I/O
- Extract shared cache directory utility (util.GetCacheDir/GetCacheFilePath)
- Add comprehensive tests for region cache (client ID mismatch, corrupt
  JSON, permissions, etc.)

Region selection priority:
1. --region flag - explicit override
2. Cached region - from previous successful auth
3. Auto-discovery - server tries regions until one succeeds

Addresses review findings:
- F1: No tests for region cache → 249 lines of tests added
- F2: No retry on stale cache → Retry logic with usingCachedHint flag
- F3: Redundant writes → Skip if region unchanged
- F4: Repeated disk I/O → Memoization via cachedRegion/regionLoaded
- F5: Duplicated cache constant → Extracted to util.CacheDirName
Copilot AI review requested due to automatic review settings March 12, 2026 08:38
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 14 out of 14 changed files in this pull request and generated 6 comments.


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

Comment on lines +37 to +53
// GetCacheFilePath returns the validated path to a cache file.
// Returns empty string if the path cannot be determined or validated.
func GetCacheFilePath(filename string) string {
cacheDir := GetCacheDir()
if cacheDir == "" {
return ""
}

filePath := filepath.Join(cacheDir, filename)

// Re-validate the full path (filename could contain traversal attempts)
sanitized, err := SanitizePath(filePath)
if err != nil {
return ""
}

return sanitized
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

GetCacheFilePath does not actually prevent escaping the cache directory when filename is an absolute path (e.g. "/etc/passwd" or "C:\Windows\..."). filepath.Join(cacheDir, filename) will drop cacheDir in that case, and SanitizePath only rejects ".." components (it does not reject absolute paths or enforce containment). This can lead callers to read/write arbitrary files if filename is ever influenced by input. Suggestion: explicitly reject absolute paths and path separators in filename, or enforce containment after joining (e.g., via filepath.Abs + filepath.Rel check).

Copilot uses AI. Check for mistakes.
Comment on lines +65 to +82
func TestGetCacheFilePath_SafeFilenames(t *testing.T) {
// This function is designed to be called with safe, constant filenames.
// The security boundary is at the caller level - callers should only pass
// known-safe filenames like "region-cache.json" or "update-check.json".
//
// Note: filepath.Join handles absolute paths by stripping the leading slash,
// so even malicious filenames like "/etc/passwd" become "cache-dir/etc/passwd"
// which stays within the cache directory.

tests := []struct {
name string
filename string
wantContains string
}{
{"simple json file", "test.json", "test.json"},
{"hyphenated name", "region-cache.json", "region-cache.json"},
{"absolute path becomes relative", "/etc/passwd", "etc/passwd"}, // stripped of leading /
}
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

The test and comment assume filepath.Join(cacheDir, "/etc/passwd") produces "cacheDir/etc/passwd" by stripping the leading slash, but Go's filepath.Join discards earlier elements when a later element is absolute (so it becomes "/etc/passwd"). As written, this test is incorrect and will fail on Unix-like systems (and is also platform-specific due to path separators). If GetCacheFilePath is meant to be safe, the expected behavior should likely be to reject absolute paths (return empty/error) rather than accept them.

Copilot uses AI. Check for mistakes.
| `ARMIS_AUTH_ENDPOINT` | Authentication service endpoint URL |
| `ARMIS_API_TOKEN` | API token for Basic authentication |
| `ARMIS_TENANT_ID` | Tenant identifier (required for Basic auth only) |
| `ARMIS_API_URL` | Override base URL for Armis API and authentication (advanced) |
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

The environment variable list was updated to add ARMIS_API_URL, but the new --region flag / ARMIS_REGION env var (added in root command) is not documented here. Please add an entry describing ARMIS_REGION (authentication region override) so users can discover it.

Suggested change
| `ARMIS_API_URL` | Override base URL for Armis API and authentication (advanced) |
| `ARMIS_API_URL` | Override base URL for Armis API and authentication (advanced) |
| `ARMIS_REGION` | Authentication region override (advanced; corresponds to `--region` flag) |

Copilot uses AI. Check for mistakes.
- `ARMIS_AUTH_ENDPOINT` - JWT authentication service endpoint URL
- `ARMIS_API_TOKEN` - API token for Basic authentication (fallback)
- `ARMIS_TENANT_ID` - Tenant identifier (required only with Basic auth; JWT extracts it from token)
- `ARMIS_API_URL` - Override base URL for Armis API (advanced; defaults based on --dev flag)
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

The env var list removes ARMIS_AUTH_ENDPOINT and adds ARMIS_API_URL, but it does not mention the newly introduced region override (--region / ARMIS_REGION). Please document ARMIS_REGION here as well to keep the CLI configuration summary accurate.

Suggested change
- `ARMIS_API_URL` - Override base URL for Armis API (advanced; defaults based on --dev flag)
- `ARMIS_API_URL` - Override base URL for Armis API (advanced; defaults based on --dev flag)
- `ARMIS_REGION` - Override Armis cloud region (equivalent to `--region`; affects default API URL selection)

Copilot uses AI. Check for mistakes.
Comment on lines +263 to +272
// This test uses the real cache directory, so we just verify the functions
// don't panic and maintain the basic contract.

// Clear any existing cache
clearCachedRegion()

// Load from empty should return false
region, ok := loadCachedRegion("test-client-pkg-level")
if ok {
// Cache might have leftover data from other tests; just verify contract
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

TestPackageLevelFunctions calls clearCachedRegion() which removes the real on-disk cache file in the user's cache directory. Unit tests should not modify user state on the machine running tests (including CI dev machines). Suggestion: temporarily replace defaultCache with a RegionCache{cacheDir: t.TempDir()} inside the test (save/restore in t.Cleanup) so package-level helpers operate on a temp location.

Suggested change
// This test uses the real cache directory, so we just verify the functions
// don't panic and maintain the basic contract.
// Clear any existing cache
clearCachedRegion()
// Load from empty should return false
region, ok := loadCachedRegion("test-client-pkg-level")
if ok {
// Cache might have leftover data from other tests; just verify contract
// Use a temporary cache directory for this test so we don't modify the user's real cache.
origCache := defaultCache
defaultCache = &RegionCache{cacheDir: t.TempDir()}
t.Cleanup(func() {
defaultCache = origCache
})
// Clear any existing cache in the temporary directory
clearCachedRegion()
// Load from empty should return false
region, ok := loadCachedRegion("test-client-pkg-level")
if ok {
// Cache might have leftover data from other tests in this temp dir; just verify contract

Copilot uses AI. Check for mistakes.
Comment on lines +171 to +188
func TestRegionCache_FilePermissions(t *testing.T) {
tempDir := t.TempDir()
cache := &RegionCache{cacheDir: tempDir}

cache.Save("client-123", testRegionUS1)

// Verify file permissions are restrictive (0600)
cachePath := filepath.Join(tempDir, regionCacheFileName)
info, err := os.Stat(cachePath)
if err != nil {
t.Fatalf("Failed to stat cache file: %v", err)
}

perm := info.Mode().Perm()
if perm != 0o600 {
t.Errorf("Expected file permissions 0600, got %o", perm)
}
}
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

TestRegionCache_FilePermissions asserts the cache file has mode 0600. This is not reliable on Windows (and sometimes on other platforms/filesystems) where Chmod/permission bits are not enforced the same way, and os.WriteFile's perm argument may be ignored. Since CI runs windows-latest, this test is likely to fail there. Suggestion: skip this assertion on Windows (runtime.GOOS == "windows"), or relax the check to only validate permissions on Unix-like systems.

Copilot uses AI. Check for mistakes.
- Skip file permissions test on Windows (Unix permissions not supported)
- Use filepath.Join for platform-agnostic path separators in test assertions
Address PR review comments from Copilot:

- Reject absolute paths and path separators in cache filename parameter
  to prevent filepath.Join from discarding the cache directory
- Add containment check to ensure result stays within cache directory
- Fix incorrect test assumptions about filepath.Join behavior
- Use temp directory in TestPackageLevelFunctions to avoid modifying
  real user cache during tests
- Document ARMIS_REGION environment variable in FEATURES.md and CLAUDE.md
Copilot AI review requested due to automatic review settings March 12, 2026 09:17
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 14 out of 14 changed files in this pull request and generated 4 comments.


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

Comment on lines +41 to +55
func GetCacheFilePath(filename string) string {
cacheDir := GetCacheDir()
if cacheDir == "" {
return ""
}

// Reject absolute paths - filepath.Join would discard cacheDir (CWE-22)
if filepath.IsAbs(filename) {
return ""
}

// Reject path separators - filename should be a simple name like "cache.json"
if strings.ContainsAny(filename, `/\`) {
return ""
}
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

GetCacheFilePath currently accepts an empty filename (or ".") and will end up returning the cache directory path itself, not a file path. That can cause callers to attempt to read/write a directory as if it were a file. Consider explicitly rejecting empty/"."/".." filenames (and ideally trimming whitespace) before joining.

Copilot uses AI. Check for mistakes.
Comment on lines +65 to +68
// Final containment check: ensure result is within cache directory
if !strings.HasPrefix(sanitized, cacheDir) {
return ""
}
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

The final containment check uses strings.HasPrefix(sanitized, cacheDir), which is not a robust path containment check (e.g., it’s sensitive to missing path separators and can be incorrect on platforms with case-insensitive paths). If you want a containment guarantee, prefer filepath.Rel-based containment (as in util.SafeJoinPath) or ensure the prefix match includes a path separator boundary; otherwise consider removing this redundant check since filename separators are already rejected.

Suggested change
// Final containment check: ensure result is within cache directory
if !strings.HasPrefix(sanitized, cacheDir) {
return ""
}
// Final containment check: ensure result is within cache directory using a robust path-based check
rel, err := filepath.Rel(cacheDir, sanitized)
if err != nil {
return ""
}
if rel == ".." || strings.HasPrefix(rel, ".."+string(os.PathSeparator)) {
return ""
}

Copilot uses AI. Check for mistakes.
Comment on lines +203 to +237
// Load cached region once per process (memoize to avoid repeated disk I/O)
if !p.regionLoaded {
if region, ok := loadCachedRegion(p.config.ClientID); ok {
p.cachedRegion = region
}
p.regionLoaded = true
}

// Determine region hint - explicit flag takes priority over cache
var regionHint *string
var usingCachedHint bool
if p.config.Region != "" {
// Explicit --region flag - don't retry on failure (user error)
regionHint = &p.config.Region
} else if p.cachedRegion != "" {
// Cached region - will retry without hint on failure
regionHint = &p.cachedRegion
usingCachedHint = true
}

result, err := p.authClient.Authenticate(ctx, p.config.ClientID, p.config.ClientSecret, regionHint)
if err != nil {
return err
// If auth failed with a cached region hint, clear cache and retry without hint
// This handles stale cache (region changed) without requiring user to re-run
if usingCachedHint {
clearCachedRegion()
p.cachedRegion = ""
// Retry without region hint - let server auto-discover
result, err = p.authClient.Authenticate(ctx, p.config.ClientID, p.config.ClientSecret, nil)
if err != nil {
return err
}
} else {
return err
}
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

The new region-caching/region-hint retry logic (clearing the cache and retrying without the hint on failure) isn’t covered by tests in this package. Adding an auth provider test that simulates a failing request when a cached region is provided and a succeeding request without the hint would help prevent regressions in this behavior.

Copilot uses AI. Check for mistakes.
CLAUDE.md Outdated
- `ARMIS_API_TOKEN` - API token for Basic authentication (fallback)
- `ARMIS_TENANT_ID` - Tenant identifier (required only with Basic auth; JWT extracts it from token)
- `ARMIS_API_URL` - Override base URL for Armis API (advanced; defaults based on --dev flag)
- `ARMIS_REGION` - Override Armis cloud region (equivalent to `--region`; affects default API URL selection)
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

This doc line says ARMIS_REGION “affects default API URL selection”, but getAPIBaseURL() currently only looks at ARMIS_API_URL and --dev; the region flag/env is passed to auth but does not influence base URL selection. Please update the wording or implement the described behavior so the docs match reality.

Suggested change
- `ARMIS_REGION` - Override Armis cloud region (equivalent to `--region`; affects default API URL selection)
- `ARMIS_REGION` - Override Armis cloud region (equivalent to `--region`; used for region-aware authentication, does not change API base URL selection)

Copilot uses AI. Check for mistakes.
Address follow-up PR review comments:

- Reject empty, whitespace-only, ".", and ".." filenames in GetCacheFilePath
  to prevent returning directory path instead of file path
- Replace strings.HasPrefix with filepath.Rel for robust path containment
  check (handles case-insensitivity and path separator edge cases)
- Add TestAuthProvider_CachedRegionRetryOnFailure to cover region-caching
  retry logic when auth fails with stale cached region
- Fix CLAUDE.md: ARMIS_REGION is for region-aware authentication,
  not API URL selection
The CLI was sending raw JWT tokens without the "Bearer" prefix,
but the backend middleware expects "Authorization: Bearer <token>"
per RFC 6750. This caused 401 errors when using JWT authentication.

Changes:
- auth.go: Return "Bearer " + token for JWT auth
- client.go: Update comments to match actual behavior
- auth_test.go: Update tests to expect Bearer prefix
Copilot AI review requested due to automatic review settings March 12, 2026 18:51
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 15 out of 15 changed files in this pull request and generated 4 comments.


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

Comment on lines +110 to +112
// Verify result is within cache directory
cacheDir := GetCacheDir()
if !strings.HasPrefix(path, cacheDir) {
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

The containment assertion here uses strings.HasPrefix(path, cacheDir), which is not a reliable path-boundary check (e.g., "/tmp/armis-cli-2" has prefix "/tmp/armis-cli"; Windows case-insensitivity/cleaning can also make this flaky). Since the production code uses filepath.Rel-based containment, the test should use a similar filepath.Rel check (or at least enforce a path separator boundary).

Suggested change
// Verify result is within cache directory
cacheDir := GetCacheDir()
if !strings.HasPrefix(path, cacheDir) {
// Verify result is within cache directory using a path-safe check
cacheDir := GetCacheDir()
rel, err := filepath.Rel(cacheDir, path)
if err != nil {
t.Fatalf("filepath.Rel(%q, %q) error: %v", cacheDir, path, err)
}
if rel == ".." || strings.HasPrefix(rel, ".."+string(os.PathSeparator)) {

Copilot uses AI. Check for mistakes.
Comment on lines +42 to +50
data, err := os.ReadFile(path) //nolint:gosec // path validated by getFilePath
if err != nil {
return "", false
}

var cache regionCacheEntry
if err := json.Unmarshal(data, &cache); err != nil {
return "", false
}
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

RegionCache.Load reads the entire cache file with os.ReadFile without any size limit. A corrupted or maliciously large cache file under the user cache dir could cause unnecessary memory usage/DoS. Consider bounding reads (e.g., check os.Stat size against a small max like 64KB, or read via io.LimitReader) before json.Unmarshal.

Copilot uses AI. Check for mistakes.
Comment on lines +223 to +237
result, err := p.authClient.Authenticate(ctx, p.config.ClientID, p.config.ClientSecret, regionHint)
if err != nil {
return err
// If auth failed with a cached region hint, clear cache and retry without hint
// This handles stale cache (region changed) without requiring user to re-run
if usingCachedHint {
clearCachedRegion()
p.cachedRegion = ""
// Retry without region hint - let server auto-discover
result, err = p.authClient.Authenticate(ctx, p.config.ClientID, p.config.ClientSecret, nil)
if err != nil {
return err
}
} else {
return err
}
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

When using a cached region hint, any Authenticate() error triggers cache clear + an immediate retry without the hint. This means transient failures (network/5xx), or genuinely invalid credentials, will always cause a second request and will also wipe a potentially-correct cache entry. Consider only clearing/retrying on a specific “region hint rejected” condition (e.g., a typed error that carries HTTP status like 401/403), and avoid retrying on transport errors.

Copilot uses AI. Check for mistakes.
Comment on lines 163 to 167
// JWT authentication
rootCmd.PersistentFlags().StringVar(&clientID, "client-id", os.Getenv("ARMIS_CLIENT_ID"), "Client ID for JWT authentication (env: ARMIS_CLIENT_ID)")
rootCmd.PersistentFlags().StringVar(&clientSecret, "client-secret", os.Getenv("ARMIS_CLIENT_SECRET"), "Client secret for JWT authentication (env: ARMIS_CLIENT_SECRET)")
rootCmd.PersistentFlags().StringVar(&authEndpoint, "auth-endpoint", os.Getenv("ARMIS_AUTH_ENDPOINT"), "Authentication service endpoint URL (env: ARMIS_AUTH_ENDPOINT)")
rootCmd.PersistentFlags().StringVar(&region, "region", os.Getenv("ARMIS_REGION"), "Override region for authentication (bypasses auto-discovery) (env: ARMIS_REGION)")

Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

This change removes the previously-supported --auth-endpoint / ARMIS_AUTH_ENDPOINT configuration surface in favor of implicit base URL selection (and ARMIS_API_URL override). If users/scripts depended on the old flag/env var, this is a breaking CLI change. Consider keeping --auth-endpoint as a deprecated alias (mapping to BaseURL / ARMIS_API_URL) for at least one release, or emitting a clear migration error when ARMIS_AUTH_ENDPOINT is set.

Copilot uses AI. Check for mistakes.
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