Skip to content

[PPSC-717] fix(sarif): stabilize ruleId to prevent recurring false positive alerts#147

Open
yiftach-armis wants to merge 4 commits intomainfrom
fix/PPSC-717-stabilize-sarif-ruleid
Open

[PPSC-717] fix(sarif): stabilize ruleId to prevent recurring false positive alerts#147
yiftach-armis wants to merge 4 commits intomainfrom
fix/PPSC-717-stabilize-sarif-ruleid

Conversation

@yiftach-armis
Copy link
Copy Markdown
Collaborator

@yiftach-armis yiftach-armis commented Apr 26, 2026

Related Issue

Type of Change

  • Bug fix (non-breaking change which fixes an issue)

Problem

The SARIF formatter uses the server-assigned finding.ID as ruleId, which bakes CWE, file path, and exact line/column numbers into the rule identity (e.g., CWE-253_armis-cli_38295677_scripts/install.ps1_95_17_95_46). When lines shift or CWE classifications evolve between scans, GitHub Code Scanning creates new alerts and ignores prior dismissals. Historical analysis of 893 alerts showed 72% (650) were duplicates caused by this unstable ruleId.

Solution

Three changes in internal/output/sarif.go:

  1. Stable ruleId: New stableRuleID() derives rule identity from CWE → CVE → FindingCategory → finding.ID fallback chain, per the SARIF 2.1.0 spec (ruleId identifies the type of check, not the instance).
  2. partialFingerprints: New computeFingerprint() adds primaryLocationLineHash (SHA-256 of ruleId + file + snippet/startLine) for GitHub's cross-run deduplication.
  3. Preserve traceability: Original server-assigned finding.ID moved to properties.findingId so no debugging context is lost.

Testing

Automated Tests

  • Unit tests added/updated
  • All tests passing locally

4 new tests added:

  • TestStableRuleID — 6-case table covering the full CWE→CVE→category→ID fallback chain
  • TestComputeFingerprint — determinism, snippet-vs-startLine behavior, collision resistance, hash format
  • TestBuildRulesStableCollapsing — two findings with same CWE collapse into one rule
  • TestSARIFFormatter_FindingIDAndFingerprints — end-to-end: ruleId=CWE, fingerprints present, findingId preserved

All existing SARIF tests pass including SARIF 2.1.0 schema validation.

Manual Testing

After merge, the next daily security scan should produce SARIF with stable ruleIds. Previously dismissed alerts should stop reappearing.

Reviewer Notes

  • The partialFingerprints.primaryLocationLineHash key is what GitHub Code Scanning uses for cross-run dedup — this is their documented mechanism.
  • When a code snippet is available, the fingerprint is snippet-based (stable across line shifts). Falls back to startLine when no snippet.
  • No changes to other formatters (human, json, junit), finding construction, or workflows.

Checklist

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

…sitive alerts

Derive ruleId from CWE/CVE/category instead of the server-assigned
finding.ID which embeds line numbers and causes duplicate alerts
when lines shift. Add partialFingerprints for cross-run dedup.
Copilot AI review requested due to automatic review settings April 26, 2026 07:46
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 26, 2026

Armis AppSecArmis AppSec Security Scan Results

🟡 Issues found

Severity Count
🟡 MEDIUM 1

Total: 1

View all 1 findings

🟡 MEDIUM (1)

CWE-253_armis-cli_38295677_internal/output/sarif.go_237_3_237_105 - In the `computeFingerprint` function the code builds a string that is fed into a SHA‑256 hash

Location: internal/output/sarif.go:237

In the computeFingerprint function the code builds a string that is fed into a SHA‑256 hash. It does this by calling fmt.Fprintf and then completely ignores the error value that fmt.Fprintf returns. If, for some reason, the write to the hash failed, the function would keep going and produce a fingerprint that might be incomplete or wrong, and the program would not notice the problem. This pattern matches the description of “incorrect check of function return value” (CWE‑253). The function is part of a command‑line tool that creates SARIF output, so it runs locally when the tool is invoked. The data used to build the fingerprint comes from the scan results, not from a direct network request, so an attacker cannot directly trigger the problem through a public interface. However, because the tool could be called by other scripts or programs, the issue is considered to have a modest exposure level. To fix the issue, the code should capture the error returned by fmt.Fprintf and handle it (for example, by returning the error up the call chain or logging it). This ensures that any unexpected write failure is not silently ignored.

CWEs: CWE-253: Incorrect Check of Function Return Value

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 26, 2026

Test Coverage Report

total: (statements) 79.3%

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:29:			Error					0.0%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:72:			copyWithContext				70.4%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:145:			WithHTTPClient				100.0%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:154:			WithUploadHTTPClient			100.0%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:162:			WithAllowLocalURLs			100.0%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:174:			NewClient				100.0%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:222:			IsDebug					100.0%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:236:			setAuthHeader				77.8%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:270:			StartIngest				72.3%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:429:			GetIngestStatus				82.6%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:470:			WaitForIngest				84.6%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:521:			FetchNormalizedResults			74.2%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:576:			FetchAllNormalizedResults		91.7%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:601:			GetScanResult				68.4%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:636:			WaitForScan				90.0%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:657:			formatBytes				100.0%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:679:			FetchArtifactScanResults		75.0%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:734:			ValidatePresignedURL			100.0%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:770:			DownloadFromPresignedURL		84.2%
github.com/ArmisSecurity/armis-cli/internal/auth/auth.go:58:			NewAuthProvider				95.2%
github.com/ArmisSecurity/armis-cli/internal/auth/auth.go:104:			GetAuthorizationHeader			100.0%
github.com/ArmisSecurity/armis-cli/internal/auth/auth.go:124:			GetTenantID				85.7%
github.com/ArmisSecurity/armis-cli/internal/auth/auth.go:141:			GetRegion				85.7%
github.com/ArmisSecurity/armis-cli/internal/auth/auth.go:156:			IsLegacy				100.0%
github.com/ArmisSecurity/armis-cli/internal/auth/auth.go:169:			GetRawToken				85.7%
github.com/ArmisSecurity/armis-cli/internal/auth/auth.go:196:			exchangeCredentials			87.9%
github.com/ArmisSecurity/armis-cli/internal/auth/auth.go:267:			refreshIfNeeded				100.0%
github.com/ArmisSecurity/armis-cli/internal/auth/auth.go:297:			parseJWTClaims				93.3%
github.com/ArmisSecurity/armis-cli/internal/auth/client.go:29:			Error					100.0%
github.com/ArmisSecurity/armis-cli/internal/auth/client.go:41:			NewAuthClient				100.0%
github.com/ArmisSecurity/armis-cli/internal/auth/client.go:97:			Authenticate				77.4%
github.com/ArmisSecurity/armis-cli/internal/auth/region_cache.go:34:		NewRegionCache				100.0%
github.com/ArmisSecurity/armis-cli/internal/auth/region_cache.go:40:		Load					82.4%
github.com/ArmisSecurity/armis-cli/internal/auth/region_cache.go:74:		Save					76.9%
github.com/ArmisSecurity/armis-cli/internal/auth/region_cache.go:104:		Clear					75.0%
github.com/ArmisSecurity/armis-cli/internal/auth/region_cache.go:114:		getFilePath				83.3%
github.com/ArmisSecurity/armis-cli/internal/auth/region_cache.go:131:		loadCachedRegion			100.0%
github.com/ArmisSecurity/armis-cli/internal/auth/region_cache.go:135:		saveCachedRegion			100.0%
github.com/ArmisSecurity/armis-cli/internal/auth/region_cache.go:139:		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/install.go:15:			init					100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/install_claude.go:34:		init					100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/install_claude.go:39:		runInstallClaude			0.0%
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:149:			SetVersion				100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:157:			Execute					100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:161:			init					100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:192:			PrintUpdateNotification			81.2%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:234:			printUpdateNotificationOnce		75.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:247:			getEnvOrDefault				100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:254:			getEnvOrDefaultInt			100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:264:			getAPIBaseURL				100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:277:			getAuthProvider				100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:289:			getPageLimit				100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:296:			validatePageLimit			100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:306:			validateFailOn				100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:324:			getFailOn				100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/scan.go:92:			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/install/claude.go:50:		NewClaudeInstaller			100.0%
github.com/ArmisSecurity/armis-cli/internal/install/claude.go:60:		InstalledVersion			100.0%
github.com/ArmisSecurity/armis-cli/internal/install/claude.go:65:		Install					10.0%
github.com/ArmisSecurity/armis-cli/internal/install/claude.go:105:		pluginCacheDir				100.0%
github.com/ArmisSecurity/armis-cli/internal/install/claude.go:111:		GetInstalledVersion			76.2%
github.com/ArmisSecurity/armis-cli/internal/install/claude.go:142:		HasExistingEnv				100.0%
github.com/ArmisSecurity/armis-cli/internal/install/claude.go:148:		fetchLatestRelease			69.6%
github.com/ArmisSecurity/armis-cli/internal/install/claude.go:188:		downloadAndExtract			71.6%
github.com/ArmisSecurity/armis-cli/internal/install/claude.go:311:		createVenv				0.0%
github.com/ArmisSecurity/armis-cli/internal/install/claude.go:340:		registerMarketplace			83.3%
github.com/ArmisSecurity/armis-cli/internal/install/claude.go:356:		registerPlugin				75.0%
github.com/ArmisSecurity/armis-cli/internal/install/claude.go:384:		enablePlugin				100.0%
github.com/ArmisSecurity/armis-cli/internal/install/claude.go:403:		validateGitHubURL			0.0%
github.com/ArmisSecurity/armis-cli/internal/install/claude.go:417:		extractFile				57.1%
github.com/ArmisSecurity/armis-cli/internal/install/claude.go:429:		writeJSON				66.7%
github.com/ArmisSecurity/armis-cli/internal/install/claude.go:440:		findPython				69.2%
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:35:		Error					0.0%
github.com/ArmisSecurity/armis-cli/internal/output/output.go:55:		GetFormatter				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/output.go:71:		ShouldFail				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/output.go:89:		CheckExit				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/sarif.go:164:		stripMarkdown				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/sarif.go:175:		Format					100.0%
github.com/ArmisSecurity/armis-cli/internal/output/sarif.go:202:		firstNonEmpty				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/sarif.go:218:		stableRuleID				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/sarif.go:233:		computeFingerprint			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/sarif.go:254:		buildRules				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/sarif.go:319:		convertToSarifResults			89.7%
github.com/ArmisSecurity/armis-cli/internal/output/sarif.go:420:		buildMessageText			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/sarif.go:427:		severityToSarifLevel			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/sarif.go:446:		severityToSecurityScore			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/sarif.go:465:		generateHelpURI				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/sarif.go:488:		convertFixToSarif			90.5%
github.com/ArmisSecurity/armis-cli/internal/output/sarif.go:605:		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:48:		NewScanner				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:63:		WithPollInterval			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:69:		WithFetchRetryInterval			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:75:		WithSBOMVEXOptions			0.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:82:		WithPullPolicy				0.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:88:		ScanImage				0.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:119:		ScanTarball				77.8%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:230:		exportImage				0.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:280:		isDockerAvailable			42.9%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:294:		getDockerCommand			75.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:303:		validateDockerCommand			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:311:		imageExistsLocally			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:319:		determinePullBehavior			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:337:		isRetryableError			75.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:345:		buildScanResult				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:372:		convertNormalizedFindings		85.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:495:		shouldFilterByExploitability		100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:514:		cleanDescription			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:533:		isEmptyFinding				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:548:		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:23:		LoadIgnorePatterns			88.9%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/ignore.go:60:		loadIgnoreFile				88.5%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/ignore.go:105:		Match					100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/ignore.go:117:		shouldSkipDir				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:46:		NewScanner				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:61:		WithPollInterval			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:67:		WithFetchRetryInterval			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:73:		WithIncludeFiles			0.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:79:		WithSBOMVEXOptions			0.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:85:		Scan					72.7%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:267:		tarGzDirectory				71.8%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:350:		isPathContained				75.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:359:		tarGzFiles				78.6%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:450:		safeAddSize				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:457:		calculateFilesSize			78.6%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:482:		calculateDirSize			79.2%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:526:		shouldSkip				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:557:		isTestFile				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:603:		isRetryableError			75.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:612:		buildScanResult				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:639:		convertNormalizedFindings		73.3%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:762:		shouldFilterByExploitability		100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:781:		cleanDescription			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:802:		generateFindingTitle			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:806:		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)				79.3%

Copy link
Copy Markdown

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

Stabilizes SARIF ruleId generation and adds partial fingerprints to reduce recurring duplicate GitHub Code Scanning alerts when line numbers/CWE classifications shift between runs.

Changes:

  • Introduces stableRuleID() and updates SARIF rule/result generation to use it (CWE → CVE → category → finding.ID fallback).
  • Adds partialFingerprints.primaryLocationLineHash computed via computeFingerprint() to improve cross-run deduplication.
  • Preserves the original server-assigned finding.ID under result.properties.findingId and adds unit/end-to-end tests for the new behavior.

Reviewed changes

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

File Description
internal/output/sarif.go Switches SARIF ruleId to a stable classification-based ID, adds partial fingerprints, and preserves server finding ID in properties.
internal/output/sarif_test.go Adds unit tests for stable rule IDs and fingerprints, plus regression/e2e coverage for SARIF output fields.

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

Comment thread internal/output/sarif.go Outdated
Comment thread internal/output/sarif.go
Comment thread internal/output/sarif.go
Comment thread internal/output/sarif.go
…antees

- stableRuleID: filter empty/whitespace values, sort for deterministic
  selection regardless of API ordering
- computeFingerprint: use length-prefixed fields to prevent separator
  collision between different tuples
- generateHelpURI: align priority with stableRuleID (CWE first, then CVE)
- Compute fingerprint from sanitized path to match emitted artifact URI
Copilot AI review requested due to automatic review settings April 26, 2026 08:59
Comment thread internal/output/sarif.go Dismissed
Copy link
Copy Markdown

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

Comments suppressed due to low confidence (1)

internal/output/sarif.go:272

  • buildRules() now deduplicates by stableRuleID, but the rule metadata (ShortDescription, DefaultConfiguration.Level, security-severity, descriptions, helpUri) is taken from whichever finding for that ruleID appears first in the findings slice. If multiple findings share the same CWE/CVE but differ in title/severity/description, the rule metadata becomes arbitrary and can vary across runs depending on finding ordering. Consider deriving rule metadata deterministically from the ruleID (or aggregating consistently, e.g., choose max severity / lexicographically smallest title).
	for _, finding := range findings {
		ruleID := stableRuleID(finding)
		if _, exists := ruleIndexMap[ruleID]; !exists {
			ruleIndexMap[ruleID] = len(rules)
			rule := sarifRule{
				ID: ruleID,
				ShortDescription: sarifMessage{
					Text: finding.Title,
				},
				DefaultConfiguration: sarifRuleConfig{
					Level: severityToSarifLevel(finding.Severity),
				},
				Properties: &sarifRuleProperties{
					SecuritySeverity: severityToSecurityScore(finding.Severity),
				},

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

Comment thread internal/output/sarif.go Outdated
Comment thread internal/output/sarif.go Outdated
Comment thread internal/output/sarif.go
Comment thread internal/output/sarif.go Outdated
- Trim whitespace on FindingCategory before using as ruleId
- Use single resolvedFile for both fingerprint and artifact URI,
  including placeholder on sanitization failure
- Capture and log sanitization error for debuggability
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.

3 participants