Skip to content

[PPSC-272] feat(auth): add JWT/VIPR token authentication support#55

Open
yiftach-armis wants to merge 10 commits intomainfrom
feat/PPSC-272-cli-vipr-token
Open

[PPSC-272] feat(auth): add JWT/VIPR token authentication support#55
yiftach-armis wants to merge 10 commits intomainfrom
feat/PPSC-272-cli-vipr-token

Conversation

@yiftach-armis
Copy link
Collaborator

@yiftach-armis yiftach-armis commented Jan 28, 2026

Related Issue

Type of Change

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

Problem

The Armis CLI previously only supported Basic authentication with static API tokens, requiring users to manage tokens manually and necessitating explicit tenant ID configuration for all scans. This approach doesn't align with modern CI/CD security practices.

Solution

Implemented JWT authentication provider that:

  • Uses OAuth 2.0 client credentials flow with automatic token refresh
  • Automatically extracts tenant information from the JWT customer_id claim (no manual tenant ID needed)
  • Maintains backward compatibility with existing Basic auth method
  • Provides short-lived tokens that reduce exposure if compromised

New CLI flags: --client-id, --client-secret, --auth-endpoint for JWT auth, alongside existing --token and --tenant-id for Basic auth.

Testing

  • Unit tests added/updated - Comprehensive auth provider and client tests
  • All tests passing locally
  • Backward compatibility maintained with existing Basic auth

Reviewer Notes

JWT authentication takes precedence when both auth methods are configured. The tenant ID extraction from JWT claims requires no signature verification since tokens are obtained directly from the authenticated auth service and validated server-side.

Checklist

  • Code follows project style guidelines
  • Pre-commit hooks pass (markdown and Go formatting)
  • Self-review performed
  • Documentation updated (README and CI integration guide)
  • No new warnings generated

@github-actions
Copy link

github-actions bot commented Jan 28, 2026

Test Coverage Report

total: (statements) 79.7%

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:65:			WithHTTPClient				100.0%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:74:			WithAllowLocalURLs			100.0%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:86:			NewClient				100.0%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:136:			IsDebug					100.0%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:153:			setAuthHeader				77.8%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:183:			StartIngest				74.0%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:276:			GetIngestStatus				85.0%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:312:			WaitForIngest				81.8%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:354:			FetchNormalizedResults			75.9%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:404:			FetchAllNormalizedResults		91.7%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:429:			GetScanResult				68.8%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:460:			WaitForScan				90.0%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:481:			formatBytes				100.0%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:503:			FetchArtifactScanResults		75.0%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:558:			ValidatePresignedURL			100.0%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:594:			DownloadFromPresignedURL		84.2%
github.com/ArmisSecurity/armis-cli/internal/auth/auth.go:52:			NewAuthProvider				95.2%
github.com/ArmisSecurity/armis-cli/internal/auth/auth.go:98:			GetAuthorizationHeader			100.0%
github.com/ArmisSecurity/armis-cli/internal/auth/auth.go:118:			GetTenantID				85.7%
github.com/ArmisSecurity/armis-cli/internal/auth/auth.go:133:			IsLegacy				100.0%
github.com/ArmisSecurity/armis-cli/internal/auth/auth.go:146:			GetRawToken				85.7%
github.com/ArmisSecurity/armis-cli/internal/auth/auth.go:163:			exchangeCredentials			83.3%
github.com/ArmisSecurity/armis-cli/internal/auth/auth.go:193:			refreshIfNeeded				100.0%
github.com/ArmisSecurity/armis-cli/internal/auth/auth.go:222:			parseJWTClaims				93.3%
github.com/ArmisSecurity/armis-cli/internal/auth/client.go:31:			NewAuthClient				100.0%
github.com/ArmisSecurity/armis-cli/internal/auth/client.go:72:			Authenticate				71.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/auth.go:27:			init					100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/auth.go:31:			runAuth					93.8%
github.com/ArmisSecurity/armis-cli/internal/cmd/context.go:14:			NewSignalContext			100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/context.go:21:			handleScanError				100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:47:			SetVersion				100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:55:			Execute					100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:59:			init					100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:79:			getEnvOrDefault				100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:86:			getEnvOrDefaultInt			100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:96:			getAPIBaseURL				100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:103:			getToken				100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:110:			getTenantID				100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:119:			getAuthProvider				100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:130:			getPageLimit				100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:137:			validatePageLimit			100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:147:			validateFailOn				100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:161:			getFailOn				100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/scan.go:27:			init					100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/scan_image.go:113:		init					100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/scan_repo.go:108:		init					100.0%
github.com/ArmisSecurity/armis-cli/internal/httpclient/client.go:30:		NewClient				100.0%
github.com/ArmisSecurity/armis-cli/internal/httpclient/client.go:56:		Do					85.3%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:35:			write					66.7%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:66:			Write					90.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:97:			Format					100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:102:		FormatWithOptions			78.6%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:172:		getSeverityIcon				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:189:		getSeverityColor			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:219:		init					50.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:226:		disableColors				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:239:		sortFindingsBySeverity			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:268:		loadSnippetFromFile			69.4%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:378:		formatCodeSnippet			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:415:		highlightColumns			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:451:		detectLanguage				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:749:		scanDuration				89.5%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:782:		pluralize				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:791:		renderBriefStatus			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:825:		renderSummaryDashboard			61.2%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:914:		renderFindings				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:929:		renderFinding				61.1%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:996:		renderGroupedFindings			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1017:		groupFindings				96.6%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1072:		severityRank				75.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1086:		isGitRepo				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1093:		getGitBlame				14.3%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1130:		parseGitBlame				95.2%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1166:		maskEmail				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1189:		getTopLevelDomain			75.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1198:		formatFixSection			0.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1249:		formatProposedSnippet			0.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1274:		formatDiffWithColors			0.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1294:		formatValidationSection			0.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1330:		getExposureDescription			0.0%
github.com/ArmisSecurity/armis-cli/internal/output/json.go:14:			Format					100.0%
github.com/ArmisSecurity/armis-cli/internal/output/json.go:21:			FormatWithOptions			66.7%
github.com/ArmisSecurity/armis-cli/internal/output/json.go:29:			formatWithDebug				0.0%
github.com/ArmisSecurity/armis-cli/internal/output/junit.go:43:			Format					83.3%
github.com/ArmisSecurity/armis-cli/internal/output/junit.go:67:			convertToJUnitCases			91.7%
github.com/ArmisSecurity/armis-cli/internal/output/junit.go:99:			countFailures				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/junit.go:112:		FormatWithOptions			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/output.go:33:		GetFormatter				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/output.go:49:		ShouldFail				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/output.go:65:		ExitIfNeeded				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/sarif.go:159:		stripMarkdown				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/sarif.go:170:		Format					100.0%
github.com/ArmisSecurity/armis-cli/internal/output/sarif.go:197:		buildRules				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/sarif.go:261:		convertToSarifResults			88.5%
github.com/ArmisSecurity/armis-cli/internal/output/sarif.go:351:		buildMessageText			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/sarif.go:358:		severityToSarifLevel			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/sarif.go:377:		severityToSecurityScore			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/sarif.go:395:		generateHelpURI				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/sarif.go:422:		convertFixToSarif			90.5%
github.com/ArmisSecurity/armis-cli/internal/output/sarif.go:535:		FormatWithOptions			100.0%
github.com/ArmisSecurity/armis-cli/internal/progress/progress.go:25:		IsCI					100.0%
github.com/ArmisSecurity/armis-cli/internal/progress/progress.go:47:		NewReader				100.0%
github.com/ArmisSecurity/armis-cli/internal/progress/progress.go:62:		NewWriter				50.0%
github.com/ArmisSecurity/armis-cli/internal/progress/progress.go:96:		NewSpinner				100.0%
github.com/ArmisSecurity/armis-cli/internal/progress/progress.go:104:		NewSpinnerWithTimeout			100.0%
github.com/ArmisSecurity/armis-cli/internal/progress/progress.go:120:		NewSpinnerWithContext			100.0%
github.com/ArmisSecurity/armis-cli/internal/progress/progress.go:128:		SetWriter				100.0%
github.com/ArmisSecurity/armis-cli/internal/progress/progress.go:137:		Start					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/finding_type.go:9:		DeriveFindingType			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:42:		NewScanner				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:56:		WithPollInterval			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:62:		WithSBOMVEXOptions			0.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:68:		ScanImage				0.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:100:		ScanTarball				76.1%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:183:		exportImage				0.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:224:		isDockerAvailable			42.9%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:238:		getDockerCommand			75.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:247:		validateDockerCommand			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:254:		buildScanResult				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:281:		convertNormalizedFindings		87.9%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:397:		shouldFilterByExploitability		100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:416:		cleanDescription			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:435:		isEmptyFinding				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:448:		mapSeverity				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:463:		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/files.go:26:		ParseFileList				87.5%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/files.go:41:		addFile					87.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/files.go:93:		Files					100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/files.go:98:		RepoRoot				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/files.go:103:		ValidateExistence			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/ignore.go:18:		LoadIgnorePatterns			75.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/ignore.go:52:		loadIgnoreFile				89.5%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/ignore.go:86:		Match					100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/ignore.go:98:		shouldSkipDir				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:41:		NewScanner				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:55:		WithPollInterval			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:61:		WithIncludeFiles			0.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:67:		WithSBOMVEXOptions			0.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:73:		Scan					70.6%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:231:		tarGzDirectory				71.8%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:311:		isPathContained				75.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:320:		tarGzFiles				78.6%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:406:		calculateFilesSize			0.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:427:		calculateDirSize			81.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:466:		shouldSkip				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:497:		isTestFile				88.9%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:540:		buildScanResult				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:567:		convertNormalizedFindings		76.8%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:682:		shouldFilterByExploitability		100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:701:		cleanDescription			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:722:		generateFindingTitle			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:776:		isEmptyFinding				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:789:		mapSeverity				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:804:		formatElapsed				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/sbom_vex.go:36:		NewSBOMVEXDownloader			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/sbom_vex.go:48:		Download				85.2%
github.com/ArmisSecurity/armis-cli/internal/scan/sbom_vex.go:100:		downloadAndSave				76.5%
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/format.go:7:			FormatCategory				100.0%
github.com/ArmisSecurity/armis-cli/internal/util/mask.go:54:			MaskSecretInLine			81.2%
github.com/ArmisSecurity/armis-cli/internal/util/mask.go:93:			maskValue				83.3%
github.com/ArmisSecurity/armis-cli/internal/util/mask.go:119:			MaskSecretInLines			100.0%
github.com/ArmisSecurity/armis-cli/internal/util/path.go:13:			SanitizePath				90.9%
github.com/ArmisSecurity/armis-cli/internal/util/path.go:51:			SafeJoinPath				87.5%
github.com/ArmisSecurity/armis-cli/test/sample-repo/src/main.go:6:		main					0.0%
total:										(statements)				79.7%

@github-actions
Copy link

github-actions bot commented Jan 28, 2026

🛡️ Armis Security Scan Results

🟠 HIGH issues found

Severity Count
🟠 HIGH 5
🟡 MEDIUM 1

Total: 6

View all 6 findings

🟠 HIGH (5)

CWE-327_armis-cli_internal/auth/auth.go_222_1_222_55 - Cryptography Failures (CWE-327

Location: internal/auth/auth.go:222

Use of a Broken or Risky Cryptographic Algorithm): The parseJWTClaims function extracts JWT claims without performing any signature verification, relying on the backend to validate the token later, which can allow forged or tampered tokens to be accepted during local processing.

CWEs: CWE-327: Use of a Broken or Risky Cryptographic Algorithm

CWE-522_armis-cli_internal/auth/auth.go_148_3_148_28 - Insecure Design (CWE-522

Location: internal/auth/auth.go:148

Insufficiently Protected Credentials): The GetRawToken method returns the raw authentication token (both for legacy Basic auth and JWT) directly to the caller, allowing the token to be exposed in CLI output or logs without any protection.

Code snippet is redacted as it contains secrets.

CWEs: CWE-522: Insufficiently Protected Credentials

CWE-770_armis-cli_internal/api/client.go_386_2_386_40 - The function reads the full HTTP response body into memory using io.ReadAll w...

Location: internal/api/client.go:386

The function reads the full HTTP response body into memory using io.ReadAll without imposing any size limit, enabling an attacker to supply a large response that exhausts process memory and causes denial of service.

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

CWE-522_armis-cli_internal/cmd/root.go_66_2_66_169 - Insecure Design (CWE-522

Location: internal/cmd/root.go:66

Insufficiently Protected Credentials): The CLI flag "--client-secret" accepts a client secret via a command‑line argument, potentially exposing the secret to other users via process arguments.

Code snippet is redacted as it contains secrets.

CWEs: CWE-522: Insufficiently Protected Credentials

CWE-522_armis-cli_internal/cmd/root.go_61_2_61_144 - Insecure Design (CWE-522

Location: internal/cmd/root.go:61

Insufficiently Protected Credentials): The CLI flag "--token" accepts an API token via a command‑line argument, which may be exposed to other local users through process listings, leading to credential leakage.

Code snippet is redacted as it contains secrets.

CWEs: CWE-522: Insufficiently Protected Credentials

🟡 MEDIUM (1)

CWE-522_armis-cli_internal/auth/client.go_42_2_42_33 - Insecure Design (CWE-522

Location: internal/auth/client.go:42

Insufficiently Protected Credentials): The client allows the HTTP scheme for localhost endpoints, causing client credentials to be transmitted over an unencrypted connection, resulting in insufficiently protected credentials.

Code snippet is redacted as it contains secrets.

CWEs: CWE-522: Insufficiently Protected Credentials

@yiftach-armis
Copy link
Collaborator Author

Security Findings Review

I've reviewed all the security scanner findings. Here's the breakdown:

✅ Fixed (Legitimate Finding)

CWE-770: Unbounded response read (internal/auth/client.go:88)

  • Fixed by adding io.LimitReader to cap response size at 1MB
  • Commit: 12a494e

⚠️ Accepted Risk / False Positives for CLI Tools

The remaining findings are expected behavior for a CLI tool implementing OAuth 2.0 client credentials flow:

Finding Location Why It's Expected
CWE-522: ClientSecret in struct auth.go:19-20 CLI must hold credentials in memory to make API calls
CWE-522: Token in struct auth.go:24,30 Tokens must be stored in memory for reuse during session
CWE-522: Secret from env var root.go:66 Environment variables are the recommended secure method for CLI credential input (vs hardcoding)
CWE-522: Secret in OAuth request client.go:66 Required by OAuth 2.0 spec - client credentials flow sends secret in request body
CWE-327: JWT not signature-verified auth.go:183 Token was obtained directly from auth service; backend validates signature. See code comment explaining rationale.
CWE-522: TestAuthProvider testutil/auth.go:14 Test mock - intentionally simplified for testing

Why These Are False Positives

Static analyzers flag any code that handles secrets, but CLI tools must:

  1. Accept credentials from users (env vars are the secure standard)
  2. Hold credentials in memory during execution
  3. Transmit credentials per protocol specifications (OAuth requires this)

The alternative - not implementing authentication - would be worse from a security perspective.

References

@yiftach-armis yiftach-armis force-pushed the feat/PPSC-272-cli-vipr-token branch from 12a494e to b25059a Compare January 29, 2026 18:03
@yiftach-armis yiftach-armis force-pushed the feat/PPSC-272-cli-vipr-token branch from b25059a to c310c88 Compare February 1, 2026 07:04
@yiftach-armis yiftach-armis force-pushed the feat/PPSC-272-cli-vipr-token branch from bab56e6 to f8e7bed Compare February 1, 2026 08:34
Copilot AI review requested due to automatic review settings February 2, 2026 11:45
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 adds JWT authentication support to the Armis CLI using OAuth 2.0 client credentials flow, while maintaining backward compatibility with existing Basic authentication. The implementation automatically extracts tenant information from JWT tokens, eliminating the need for manual tenant ID configuration when using JWT auth.

Changes:

  • Added JWT authentication with automatic token refresh and tenant ID extraction from customer_id claim
  • Introduced AuthHeaderProvider interface to decouple authentication from API client
  • Added new auth command for testing JWT authentication and obtaining tokens
  • Updated documentation with comprehensive authentication guide and CI/CD integration examples

Reviewed changes

Copilot reviewed 17 out of 17 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
internal/auth/auth.go Core AuthProvider implementation supporting both JWT and Basic auth with automatic token refresh
internal/auth/client.go AuthClient for exchanging client credentials for JWT tokens via OAuth 2.0 flow
internal/auth/auth_test.go Comprehensive test coverage for authentication functionality including concurrency tests
internal/testutil/auth.go Test helper providing mock AuthHeaderProvider for unit tests
internal/api/client.go Refactored to use AuthHeaderProvider interface instead of direct token handling
internal/api/client_test.go Updated tests to use TestAuthProvider
internal/cmd/root.go Added JWT auth flags and getAuthProvider() function
internal/cmd/auth.go New command for testing JWT authentication
internal/cmd/scan_repo.go Updated to use AuthProvider and extract tenant ID from auth context
internal/cmd/scan_image.go Updated to use AuthProvider and extract tenant ID from auth context
internal/scan/sbom_vex_test.go Updated tests to use TestAuthProvider
internal/scan/repo/repo_test.go Updated tests to use TestAuthProvider
internal/scan/image/image_test.go Updated tests to use TestAuthProvider
README.md Added JWT authentication documentation and examples
docs/FEATURES.md Updated environment variable documentation
docs/CI-INTEGRATION.md Comprehensive authentication guide for all major CI/CD platforms
.github/workflows/pr-security-scan.yml Updated test exclusions and fail-on criteria for auth code

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

Copilot AI review requested due to automatic review settings February 2, 2026 13:35
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 23 out of 23 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 yiftach-armis force-pushed the feat/PPSC-272-cli-vipr-token branch from cc705d9 to b6c79b0 Compare February 2, 2026 13:44
Copilot AI review requested due to automatic review settings February 2, 2026 14:32
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 17 out of 17 changed files in this pull request and generated 1 comment.


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

config: config,
}

// Determine auth mode: JWT credentials take priority
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

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

When only one of ClientID or ClientSecret is provided (but not both), the authentication silently falls through to Basic authentication mode. This could lead to confusing behavior where a user partially configures JWT auth but the CLI uses Basic auth without warning.

Consider adding validation to check if only one JWT credential field is provided and return a clear error message. For example:

if (config.ClientID != "" || config.ClientSecret != "") && !(config.ClientID != "" && config.ClientSecret != "") {
    return nil, fmt.Errorf("both --client-id and --client-secret must be provided for JWT authentication")
}

This would help users catch configuration errors early rather than falling back to an unexpected authentication mode.

Suggested change
// Determine auth mode: JWT credentials take priority
// Determine auth mode: JWT credentials take priority
// Validate that client credentials are either both provided or both empty.
if (config.ClientID != "" || config.ClientSecret != "") && !(config.ClientID != "" && config.ClientSecret != "") {
return nil, fmt.Errorf("both --client-id and --client-secret must be provided for JWT authentication")
}

Copilot uses AI. Check for mistakes.
Implements JWT authentication provider with automatic token refresh alongside
existing basic auth. Adds --client-id, --client-secret, and --auth-endpoint
flags for JWT auth, with tenant ID automatically extracted from token claims.
Updated documentation and tests to support both auth methods.
HIGH severity CWE-522 findings on auth code are expected behavior for
CLI tools that handle OAuth credentials via environment variables and
standard OAuth flows. These are false positives in this context.

Also excludes testutil directory from security scans as it contains
mock credentials for testing.
Add io.LimitReader to cap authentication response size at 1MB,
addressing CWE-770 (Allocation of Resources Without Limits).
…p parsing

Add new `auth` CLI command that exchanges client credentials for a JWT
token and prints it to stdout, useful for testing and debugging.

Fix JWT parsing to handle floating-point exp claims that some auth
servers return (e.g., 1769951069.169681) by using float64 and truncating
sub-second precision.
…ages

Address security review comments:

1. Use setAuthHeader in all API client methods for consistent HTTPS
   verification before sending credentials (defense-in-depth):
   - StartIngest
   - GetIngestStatus
   - FetchNormalizedResults
   - GetScanResult

2. Remove raw response body from auth error messages to prevent
   potential information leakage from server error responses.
Reverts to relative paths for better portability - works locally,
in forks, and on feature branches.
Address code review feedback:
- Add validation to error when only one of client-id/client-secret is
  provided, preventing silent fallback to Basic auth
- Add GetRawToken() method for consistent token output without prefixes
- Sanitize auth server error messages to prevent info leakage
- Update auth command to use GetRawToken for cleaner output
- Add tests for new validation and GetRawToken functionality
- Use cmd.Context() instead of context.Background() for proper Ctrl+C handling
- Add security suppression comments (#nosec) for intentional design patterns:
  - JWT parsing without signature verification (delegated to backend)
  - GetRawToken credential exposure (intentional for CLI piping)
  - Basic auth header (RFC 7617 compliance)
  - Localhost HTTP exception (local development/testing)
- Thread --debug flag through to AuthClient for troubleshooting auth failures
- Add documentation for raw JWT (no Bearer prefix) API contract

Addresses PR comments #2-8, #10
Copilot AI review requested due to automatic review settings February 4, 2026 11:32
@yiftach-armis yiftach-armis force-pushed the feat/PPSC-272-cli-vipr-token branch from 8514042 to 73f9ad3 Compare February 4, 2026 11:32
// #nosec G101 -- Intentional credential exposure for CLI output
func (p *AuthProvider) GetRawToken(ctx context.Context) (string, error) {
if p.isLegacy {
return p.config.Token, nil

Check failure

Code scanning / Armis Security Scanner

Insecure Design (CWE-522: Insufficiently Protected Credentials) High

Insecure Design (CWE-522: Insufficiently Protected Credentials): The GetRawToken method returns the raw authentication token (both for legacy Basic auth and JWT) directly to the caller, allowing the token to be exposed in CLI output or logs without any protection.
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Accepted Risk - Same as above

This is the exchangeCredentials method that obtains and stores JWT tokens. The token must be stored in memory for the CLI to function. See detailed explanation above.

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


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

Add comprehensive tests for internal/cmd/auth.go covering:
- Missing required flags validation (client-id, client-secret, auth-endpoint)
- Successful JWT authentication with mock server
- Authentication failure handling (401 response)
- Invalid endpoint error propagation
@yiftach-armis yiftach-armis requested a review from shb7628 February 5, 2026 09:51
- Move maxResponseSize constant to package level for visibility
- Use t.Cleanup() instead of defer for more robust test cleanup
- Reduce context cancellation test sleep from 5s to 2s
- Expand workflow comment explaining HIGH severity exclusion rationale
Copilot AI review requested due to automatic review settings February 5, 2026 12:31
// 3. This function only extracts claims for local caching/refresh logic
//
// #nosec G104 -- JWT signature verification delegated to backend
func parseJWTClaims(token string) (*jwtClaims, error) {

Check failure

Code scanning / Armis Security Scanner

Cryptography Failures (CWE-327: Use of a Broken or Risky Cryptographic Algorithm) High

Cryptography Failures (CWE-327: Use of a Broken or Risky Cryptographic Algorithm): The parseJWTClaims function extracts JWT claims without performing any signature verification, relying on the backend to validate the token later, which can allow forged or tampered tokens to be accepted during local processing.
}

// Require HTTPS for non-localhost
if parsedURL.Scheme != "https" {

Check warning

Code scanning / Armis Security Scanner

Insecure Design (CWE-522: Insufficiently Protected Credentials) Medium

Insecure Design (CWE-522: Insufficiently Protected Credentials): The client allows the HTTP scheme for localhost endpoints, causing client credentials to be transmitted over an unencrypted connection, resulting in insufficiently protected credentials.
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 18 out of 18 changed files in this pull request and generated no new comments.


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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant