Skip to content

fix(feature): add SHA-256 integrity verification for direct tar downloads#89

Merged
skevetter merged 5 commits into
mainfrom
fix/feature-tarball-integrity
Apr 24, 2026
Merged

fix(feature): add SHA-256 integrity verification for direct tar downloads#89
skevetter merged 5 commits into
mainfrom
fix/feature-tarball-integrity

Conversation

@skevetter

@skevetter skevetter commented Apr 24, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds SHA-256 integrity verification for feature tarballs downloaded via direct HTTP URL in processDirectTarFeature()
  • After download, computes SHA-256 hash using hash.File() and stores it as a feature.sha256 sidecar file alongside the cached tarball
  • On cache hits, verifies the cached tarball against the stored hash — mismatch triggers cache eviction and re-download
  • Missing hash files (backward compat with older caches) log a warning and allow the cached content to be used
  • Extracts verifyCacheIntegrity(), storeIntegrityHash(), and extractTarball() as helper functions to keep processDirectTarFeature() under funlen limits
  • Adds 4 unit tests covering: sidecar write, valid hash verification, corrupted hash re-download trigger, and missing hash backward compatibility

Summary by CodeRabbit

  • New Features

    • Added SHA-256 integrity validation for cached feature downloads to ensure data integrity.
    • Cached features are automatically re-downloaded if integrity verification fails.
    • Maintains backward compatibility with existing cached features when checksum sidecar is absent.
  • Tests

    • Added unit tests for integrity read/write and verification behaviors.
    • Added an end-to-end test confirming cache reuse across workspaces.

@netlify

netlify Bot commented Apr 24, 2026

Copy link
Copy Markdown

Deploy Preview for devsydev canceled.

Name Link
🔨 Latest commit 71c3692
🔍 Latest deploy log https://app.netlify.com/projects/devsydev/deploys/69ead39f8454ed000859a0f6

@coderabbitai

coderabbitai Bot commented Apr 24, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds SHA-256 integrity checks for cached feature tarballs, persisting/verifying sidecar hashes, deleting invalid caches and re-downloading, and centralizes extraction into a helper. Missing sidecars remain valid for backward compatibility.

Changes

Cohort / File(s) Summary
Feature cache & extraction
pkg/devcontainer/feature/features.go
Adds SHA-256 sidecar read/write and verification before reusing extracted/ cache; deletes cache on mismatch and triggers fresh download. Introduces a shared extractTarball helper (with cleanup on failure) and removes the prior existence-only cache short-circuit.
Integrity unit tests
pkg/devcontainer/feature/features_integrity_test.go
New testify suite that creates a test feature.tgz, asserts storeIntegrityHash writes the correct .sha256, and verifies verifyCacheIntegrity behavior for matching, corrupted, and missing .sha256 cases.
End-to-end caching test
e2e/tests/up-features/up_features.go
New e2e test serving a local feature tarball via a mock HTTP server and running two DevsyUp runs across distinct workspaces to assert cached reuse (server receives a single GET).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and specifically describes the main change: adding SHA-256 integrity verification for direct tar feature downloads, which matches the primary objective across all modified files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

Copy link
Copy Markdown

⚠️ This PR contains unsigned commits. To get your PR merged, please sign those commits (git rebase --exec 'git commit -S --amend --no-edit -n' @{upstream}) and force push them to this branch (git push --force-with-lease).

If you're new to commit signing, there are different ways to set it up:

Sign commits with gpg

Follow the steps below to set up commit signing with gpg:

  1. Generate a GPG key
  2. Add the GPG key to your GitHub account
  3. Configure git to use your GPG key for commit signing
Sign commits with ssh-agent

Follow the steps below to set up commit signing with ssh-agent:

  1. Generate an SSH key and add it to ssh-agent
  2. Add the SSH key to your GitHub account
  3. Configure git to use your SSH key for commit signing
Sign commits with 1Password

You can also sign commits using 1Password, which lets you sign commits with biometrics without the signing key leaving the local 1Password process.

Learn how to use 1Password to sign your commits.

Watch the demo

skevetter added a commit that referenced this pull request Apr 24, 2026
…oads (#89)

Downloaded feature tarballs via HTTP had no integrity verification.
Compute SHA-256 after download and store as a sidecar file; on cache
hits, verify the cached tarball against the stored hash. Hash mismatch
triggers re-download. Missing hash files (backward compat) log a
warning and continue.
@skevetter
skevetter force-pushed the fix/feature-tarball-integrity branch from ab4bffb to 7494ed6 Compare April 24, 2026 01:05

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (2)
pkg/devcontainer/feature/features.go (2)

124-147: Consider unifying the cache-hit path across OCI and direct-tar features.

processOCIFeature calls checkFeatureCache (line 152) while processDirectTarFeature now inlines its own cache-hit logic (lines 372-381). This is where the feature.json regression flagged above slipped in, and it also means any future cache-policy change has to be made in two places. A small refactor — e.g., extending checkFeatureCache with an optional integrityCheck func(featureFolder string) bool parameter, or introducing checkFeatureCacheWithIntegrity — would let both call sites share the "extracted exists && feature.json present && [optional integrity ok]" logic.

Also applies to: 372-381

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/devcontainer/feature/features.go` around lines 124 - 147, The cache-hit
logic is duplicated between checkFeatureCache and the inline code in
processDirectTarFeature; refactor to unify by extending checkFeatureCache to
accept an optional integrityCheck func(featureFolder string) bool (or add a new
checkFeatureCacheWithIntegrity wrapper) and update both processOCIFeature and
processDirectTarFeature to call the unified function; implement
checkFeatureCache to verify the "extracted" folder exists, ensure
config.DEVCONTAINER_FEATURE_FILE_NAME (feature.json) is present, and if provided
call integrityCheck to validate contents, returning the extracted folder and
true only when all checks pass and removing the cacheFolder on failure as the
current logic does.

288-297: Consider log.Debugf for the missing-sidecar branch.

For every cache hit on a pre-upgrade feature, this path fires a Warn. During migration that can flood logs with "No integrity hash..." for entries users can't act on. Since this is the documented backward-compat contract (not a real problem), Debugf is a better fit; reserve Warn for situations the operator might want to address.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/devcontainer/feature/features.go` around lines 288 - 297, In
verifyCacheIntegrity, the branch that handles a missing stored hash currently
calls log.Warnf which floods logs for expected pre-upgrade cached features;
change that call to log.Debugf (keeping the same message and formatting) so
missing-sidecar/backward-compat cache entries are logged at debug level; locate
the call that references featureFolder/hashFile and the id parameter inside
verifyCacheIntegrity and replace Warnf with Debugf.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pkg/devcontainer/feature/features_integrity_test.go`:
- Around line 33-35: Add a targeted gosec suppression for G304 on the
os.ReadFile call: in features_integrity_test.go, locate the variable/hashFile
and the call to os.ReadFile(hashFile) and append a line-level comment
"//nolint:gosec" to that call so the linter ignores the false-positive on this
test-only temporary path.

In `@pkg/devcontainer/feature/features.go`:
- Around line 318-333: storeIntegrityHash currently swallows os.WriteFile errors
which lets processDirectTarFeature continue and leaves an unverifiable cache for
future runs; change storeIntegrityHash to return an error (or a boolean) instead
of logging-and-returning, and update its caller (processDirectTarFeature) to
treat a write failure like a download failure: either propagate the error up to
abort installation or perform cleanup by removing featureFolder and the
downloaded feature.tgz so verifyCacheIntegrity cannot trust the incomplete
cache; reference storeIntegrityHash, processDirectTarFeature and
verifyCacheIntegrity when making these changes.
- Around line 335-340: The os.Open call in extractTarball is flagged by gosec
G304 but downloadFile is built internally (featureFolder + "feature.tgz");
annotate the os.Open invocation in func extractTarball with a //nolint:gosec
comment (same pattern as pkg/hash/hash.go's File method) to suppress G304,
keeping the current error handling and deferred file.Close unchanged.
- Around line 372-381: The direct-tar cache path now only checks
os.Stat(featureExtractedFolder) and verifyCacheIntegrity, but it must also
ensure the extracted folder contains config.DEVCONTAINER_FEATURE_FILE_NAME like
checkFeatureCache and processOCIFeature do; update the branch in features.go
(the block using featureExtractedFolder/featureFolder and calling
verifyCacheIntegrity) to stat for the feature JSON inside featureExtractedFolder
(config.DEVCONTAINER_FEATURE_FILE_NAME) and treat a missing file as a cache miss
by removing featureFolder (same behavior as checkFeatureCache) before
proceeding.

---

Nitpick comments:
In `@pkg/devcontainer/feature/features.go`:
- Around line 124-147: The cache-hit logic is duplicated between
checkFeatureCache and the inline code in processDirectTarFeature; refactor to
unify by extending checkFeatureCache to accept an optional integrityCheck
func(featureFolder string) bool (or add a new checkFeatureCacheWithIntegrity
wrapper) and update both processOCIFeature and processDirectTarFeature to call
the unified function; implement checkFeatureCache to verify the "extracted"
folder exists, ensure config.DEVCONTAINER_FEATURE_FILE_NAME (feature.json) is
present, and if provided call integrityCheck to validate contents, returning the
extracted folder and true only when all checks pass and removing the cacheFolder
on failure as the current logic does.
- Around line 288-297: In verifyCacheIntegrity, the branch that handles a
missing stored hash currently calls log.Warnf which floods logs for expected
pre-upgrade cached features; change that call to log.Debugf (keeping the same
message and formatting) so missing-sidecar/backward-compat cache entries are
logged at debug level; locate the call that references featureFolder/hashFile
and the id parameter inside verifyCacheIntegrity and replace Warnf with Debugf.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9cc88618-4114-44e2-980d-9550b61a144a

📥 Commits

Reviewing files that changed from the base of the PR and between ea9f774 and ab4bffb.

📒 Files selected for processing (2)
  • pkg/devcontainer/feature/features.go
  • pkg/devcontainer/feature/features_integrity_test.go

Comment on lines +33 to +35
hashFile := filepath.Join(dir, "feature.sha256")
stored, err := os.ReadFile(hashFile)
s.Require().NoError(err)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Lint Gate failure: add a //nolint:gosec annotation for G304.

The pipeline's Lint Gate fails at line 34 on os.ReadFile(hashFile) (gosec G304). hashFile is a test-constructed path under TempDir, so this is a false positive, but CI still blocks the merge. A targeted suppression keeps the tree clean without broadening the exception.

🔧 Proposed fix
 	hashFile := filepath.Join(dir, "feature.sha256")
-	stored, err := os.ReadFile(hashFile)
+	stored, err := os.ReadFile(hashFile) //nolint:gosec // test-controlled path under t.TempDir()
 	s.Require().NoError(err)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
hashFile := filepath.Join(dir, "feature.sha256")
stored, err := os.ReadFile(hashFile)
s.Require().NoError(err)
hashFile := filepath.Join(dir, "feature.sha256")
stored, err := os.ReadFile(hashFile) //nolint:gosec // test-controlled path under t.TempDir()
s.Require().NoError(err)
🧰 Tools
🪛 GitHub Check: Lint Gate

[failure] 34-34:
G304: Potential file inclusion via variable (gosec)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/devcontainer/feature/features_integrity_test.go` around lines 33 - 35,
Add a targeted gosec suppression for G304 on the os.ReadFile call: in
features_integrity_test.go, locate the variable/hashFile and the call to
os.ReadFile(hashFile) and append a line-level comment "//nolint:gosec" to that
call so the linter ignores the false-positive on this test-only temporary path.

Comment thread pkg/devcontainer/feature/features.go
Comment on lines +335 to +340
func extractTarball(downloadFile, dest string) error {
file, err := os.Open(downloadFile)
if err != nil {
return fmt.Errorf("open tarball: %w", err)
}
defer func() { _ = file.Close() }()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Lint Gate failure: add a //nolint:gosec annotation for G304 on os.Open.

Same pipeline failure as the test file. downloadFile is constructed internally from featureFolder/"feature.tgz", not user input, but gosec cannot tell. pkg/hash/hash.go already uses this pattern for its own os.Open (see the relevant snippet of hash.File).

🔧 Proposed fix
 func extractTarball(downloadFile, dest string) error {
-	file, err := os.Open(downloadFile)
+	file, err := os.Open(downloadFile) //nolint:gosec // downloadFile is derived from the internal feature cache dir, not user input
 	if err != nil {
 		return fmt.Errorf("open tarball: %w", err)
 	}
🧰 Tools
🪛 GitHub Check: Lint Gate

[failure] 336-336:
G304: Potential file inclusion via variable (gosec)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/devcontainer/feature/features.go` around lines 335 - 340, The os.Open
call in extractTarball is flagged by gosec G304 but downloadFile is built
internally (featureFolder + "feature.tgz"); annotate the os.Open invocation in
func extractTarball with a //nolint:gosec comment (same pattern as
pkg/hash/hash.go's File method) to suppress G304, keeping the current error
handling and deferred file.Close unchanged.

Comment on lines +372 to 381

// Check cache — verify integrity if present.
_, statErr := os.Stat(featureExtractedFolder)
if statErr == nil && !forceDownload {
if verifyCacheIntegrity(featureFolder, id) {
log.Debugf("direct tar feature already cached: folder=%s", featureExtractedFolder)
return featureExtractedFolder, nil
}
_ = os.RemoveAll(featureFolder)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Regression: extracted-folder sanity check (feature.json presence) was dropped for direct-tar features.

Previously the direct-tar path went through checkFeatureCache (lines 131-144), which stats extracted/ and verifies that config.DEVCONTAINER_FEATURE_FILE_NAME is present — removing the whole cache if the extraction looked incomplete. The new flow only checks os.Stat(featureExtractedFolder) plus the tarball hash. If a prior run died mid-extraction (mkdir succeeded, feature.json never written), the tarball + sidecar are still consistent with each other and this branch will happily return an empty/partial extracted directory to the caller, breaking downstream installation. processOCIFeature (line 152) still has this guard via checkFeatureCache; the two paths should behave the same.

🛡️ Proposed fix — reinstate the feature.json presence check
 	// Check cache — verify integrity if present.
-	_, statErr := os.Stat(featureExtractedFolder)
-	if statErr == nil && !forceDownload {
-		if verifyCacheIntegrity(featureFolder, id) {
-			log.Debugf("direct tar feature already cached: folder=%s", featureExtractedFolder)
-			return featureExtractedFolder, nil
-		}
-		_ = os.RemoveAll(featureFolder)
+	if !forceDownload {
+		if _, statErr := os.Stat(featureExtractedFolder); statErr == nil {
+			_, fErr := os.Stat(filepath.Join(featureExtractedFolder, config.DEVCONTAINER_FEATURE_FILE_NAME))
+			if fErr == nil && verifyCacheIntegrity(featureFolder, id) {
+				log.Debugf("direct tar feature already cached: folder=%s", featureExtractedFolder)
+				return featureExtractedFolder, nil
+			}
+			_ = os.RemoveAll(featureFolder)
+		}
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Check cache — verify integrity if present.
_, statErr := os.Stat(featureExtractedFolder)
if statErr == nil && !forceDownload {
if verifyCacheIntegrity(featureFolder, id) {
log.Debugf("direct tar feature already cached: folder=%s", featureExtractedFolder)
return featureExtractedFolder, nil
}
_ = os.RemoveAll(featureFolder)
}
// Check cache — verify integrity if present.
if !forceDownload {
if _, statErr := os.Stat(featureExtractedFolder); statErr == nil {
_, fErr := os.Stat(filepath.Join(featureExtractedFolder, config.DEVCONTAINER_FEATURE_FILE_NAME))
if fErr == nil && verifyCacheIntegrity(featureFolder, id) {
log.Debugf("direct tar feature already cached: folder=%s", featureExtractedFolder)
return featureExtractedFolder, nil
}
_ = os.RemoveAll(featureFolder)
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/devcontainer/feature/features.go` around lines 372 - 381, The direct-tar
cache path now only checks os.Stat(featureExtractedFolder) and
verifyCacheIntegrity, but it must also ensure the extracted folder contains
config.DEVCONTAINER_FEATURE_FILE_NAME like checkFeatureCache and
processOCIFeature do; update the branch in features.go (the block using
featureExtractedFolder/featureFolder and calling verifyCacheIntegrity) to stat
for the feature JSON inside featureExtractedFolder
(config.DEVCONTAINER_FEATURE_FILE_NAME) and treat a missing file as a cache miss
by removing featureFolder (same behavior as checkFeatureCache) before
proceeding.

…oads (#89)

Downloaded feature tarballs via HTTP had no integrity verification.
Compute SHA-256 after download and store as a sidecar file; on cache
hits, verify the cached tarball against the stored hash. Hash mismatch
triggers re-download. Missing hash files (backward compat) log a
warning and continue.
Use filepath.Clean on file paths passed to os.Open and os.ReadFile
to satisfy gosec G304 (potential file inclusion via variable).
@skevetter
skevetter force-pushed the fix/feature-tarball-integrity branch from 8084957 to bfafb7f Compare April 24, 2026 01:18
Extend the up-features e2e suite with a test that exercises the SHA-256
integrity verification path. The test downloads a feature tarball via HTTP,
then creates a second workspace with the same feature URL and asserts
that the cached tarball is reused (integrity check passes, no re-download).
Use filepath.Clean on variable paths passed to os.ReadFile to satisfy
gosec G304 (potential file inclusion via variable).
CopyToTempDir changes the working directory, so the second copy could
not resolve its relative testdata path. Chdir back to initialDir
between the two copy calls.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
e2e/tests/up-features/up_features.go (1)

164-169: Optional: strengthen the cache-hit assertion with a positive feature check.

The single-handler + HaveLen(1) assertion already proves no second download was issued, which is the primary invariant under test. For extra signal (and to match the style of the existing "http headers download" spec above), consider adding a DevsySSH probe after the second DevsyUp to confirm the cached feature was actually installed into the new workspace (e.g., the same install check the neighboring specs perform). Not blocking.

Also applies to: 198-199

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/tests/up-features/up_features.go` around lines 164 - 169, Add a positive
cache-hit check after the second DevsyUp by invoking the same DevsySSH probe
used in the neighboring specs to confirm the feature was actually installed from
cache (e.g., call DevsySSH to run the install verification command after the
second DevsyUp); update the test that uses server.AppendHandlers / HaveLen(1) to
include this DevsySSH probe so it asserts not only that no second download
occurred but also that the cached feature is present—apply the same change
pattern to the other occurrence around the 198-199 block.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@e2e/tests/up-features/up_features.go`:
- Around line 164-169: Add a positive cache-hit check after the second DevsyUp
by invoking the same DevsySSH probe used in the neighboring specs to confirm the
feature was actually installed from cache (e.g., call DevsySSH to run the
install verification command after the second DevsyUp); update the test that
uses server.AppendHandlers / HaveLen(1) to include this DevsySSH probe so it
asserts not only that no second download occurred but also that the cached
feature is present—apply the same change pattern to the other occurrence around
the 198-199 block.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4348a49d-b867-4505-abe2-3a869b61bd52

📥 Commits

Reviewing files that changed from the base of the PR and between ab4bffb and 71c3692.

📒 Files selected for processing (3)
  • e2e/tests/up-features/up_features.go
  • pkg/devcontainer/feature/features.go
  • pkg/devcontainer/feature/features_integrity_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • pkg/devcontainer/feature/features.go

@skevetter
skevetter merged commit ae95ec8 into main Apr 24, 2026
47 checks passed
@skevetter
skevetter deleted the fix/feature-tarball-integrity branch April 24, 2026 03:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant