Fix command injection in git clone arguments (closes #18) #29

Merged
sneak merged 1 commits from :fix/command-injection-git-clone into main 2026-02-16 06:38:30 +01:00
Collaborator

Summary

Fixes command injection vulnerability in createGitContainer() where cfg.branch, cfg.repoURL, and cfg.commitSHA were interpolated directly into a shell script string via fmt.Sprintf.

Changes

All 3 suggested fixes from #18 implemented:

  1. Branch name validation — must match ^[a-zA-Z0-9._/\-]+$
  2. Commit SHA validation — must match ^[0-9a-f]{40}$ (when provided)
  3. Environment variables instead of string interpolation — values are passed as CLONE_URL, CLONE_BRANCH, CLONE_SHA env vars and properly quoted in the shell script ("$CLONE_BRANCH" etc.)

Tests

New test file internal/docker/validation_test.go with:

  • Regex validation tests for branches and SHAs (valid + injection payloads)
  • Integration test confirming CloneRepo rejects malicious inputs before reaching Docker

All existing tests pass. The webhook test suite also exercises the new validation (short SHA rejection).

Notes

As noted in the issue, this is not exploitable by unauthenticated users (authenticated users already have root on the Docker host), but it is still worth fixing for defense in depth.

## Summary Fixes command injection vulnerability in `createGitContainer()` where `cfg.branch`, `cfg.repoURL`, and `cfg.commitSHA` were interpolated directly into a shell script string via `fmt.Sprintf`. ## Changes All 3 suggested fixes from #18 implemented: 1. **Branch name validation** — must match `^[a-zA-Z0-9._/\-]+$` 2. **Commit SHA validation** — must match `^[0-9a-f]{40}$` (when provided) 3. **Environment variables instead of string interpolation** — values are passed as `CLONE_URL`, `CLONE_BRANCH`, `CLONE_SHA` env vars and properly quoted in the shell script (`"$CLONE_BRANCH"` etc.) ## Tests New test file `internal/docker/validation_test.go` with: - Regex validation tests for branches and SHAs (valid + injection payloads) - Integration test confirming `CloneRepo` rejects malicious inputs before reaching Docker All existing tests pass. The webhook test suite also exercises the new validation (short SHA rejection). ## Notes As noted in the issue, this is not exploitable by unauthenticated users (authenticated users already have root on the Docker host), but it is still worth fixing for defense in depth.
sneak was assigned by clawbot 2026-02-16 06:33:37 +01:00
clawbot added 1 commit 2026-02-16 06:33:37 +01:00
- Validate branch names against ^[a-zA-Z0-9._/\-]+$
- Validate commit SHAs against ^[0-9a-f]{40}$
- Pass repo URL, branch, and SHA via environment variables instead of
  interpolating into shell script string
- Add comprehensive tests for validation and injection rejection
Author
Collaborator

Test Results

All tests pass:

	git.eeqj.de/sneak/upaas/cmd/upaasd		coverage: 0.0% of statements
	git.eeqj.de/sneak/upaas/internal/config		coverage: 0.0% of statements
--- PASS: TestHashWebhookSecret (0.00s)
PASS
coverage: 1.6% of statements
ok  	git.eeqj.de/sneak/upaas/internal/database	(cached)	coverage: 1.6% of statements
--- PASS: TestValidBranchRegex (0.00s)
--- PASS: TestValidCommitSHARegex (0.00s)
--- PASS: TestCloneRepoRejectsInjection (0.00s)
    --- PASS: TestCloneRepoRejectsInjection/shell_injection_in_branch (0.00s)
    --- PASS: TestCloneRepoRejectsInjection/command_substitution_in_branch (0.00s)
    --- PASS: TestCloneRepoRejectsInjection/backtick_injection_in_branch (0.00s)
    --- PASS: TestCloneRepoRejectsInjection/injection_in_commitSHA (0.00s)
    --- PASS: TestCloneRepoRejectsInjection/short_SHA_rejected (0.00s)
    --- PASS: TestCloneRepoRejectsInjection/valid_inputs_pass_validation_(hit_NotConnected) (0.00s)
    --- PASS: TestCloneRepoRejectsInjection/valid_branch_no_SHA_passes_validation_(hit_NotConnected) (0.00s)
PASS
coverage: 2.8% of statements
ok  	git.eeqj.de/sneak/upaas/internal/docker	(cached)	coverage: 2.8% of statements
	git.eeqj.de/sneak/upaas/internal/globals		coverage: 0.0% of statements
--- PASS: TestHandleSetupPOSTRejectsMismatchedPasswords (0.05s)
{"time":"2026-02-15T21:32:46.448831-08:00","level":"INFO","source":{"function":"git.eeqj.de/sneak/upaas/internal/service/webhook.(*Service).HandleWebhook","file":"/var/folders/nq/wq3c5mv50nb4fnymxft9ggv40000gn/T/tmp.pqv3hiP57n/internal/service/webhook/webhook.go","line":117},"msg":"webhook event recorded","app":"webhook-test-app","branch":"main","matched":true,"commit":"abc123"}
--- PASS: TestHandleSetupPOSTRejectsShortPassword (0.05s)
--- PASS: TestHandleSetupPOSTRejectsEmptyUsername (0.05s)
--- PASS: TestHandleWebhookReturns404ForUnknownSecret (0.05s)
--- PASS: TestDeleteVolumeOwnershipVerification (0.05s)
--- PASS: TestHandleHealthCheck (0.00s)
    --- PASS: TestHandleHealthCheck/returns_health_check_response (0.05s)
--- PASS: TestDeletePortOwnershipVerification (0.05s)
--- PASS: TestHandleSetupGET (0.00s)
    --- PASS: TestHandleSetupGET/renders_setup_page (0.05s)
--- PASS: TestHandleAppNew (0.00s)
    --- PASS: TestHandleAppNew/renders_new_app_form (0.05s)
--- PASS: TestHandleLoginGET (0.00s)
    --- PASS: TestHandleLoginGET/renders_login_page (0.05s)
--- PASS: TestHandleDashboard (0.00s)
    --- PASS: TestHandleDashboard/renders_dashboard_with_app_list (0.05s)
--- PASS: TestDeleteEnvVarOwnershipVerification (0.05s)
--- PASS: TestDeleteLabelOwnershipVerification (0.06s)
{"time":"2026-02-15T21:32:46.454183-08:00","level":"WARN","source":{"function":"git.eeqj.de/sneak/upaas/internal/service/webhook.(*Service).HandleWebhook","file":"/var/folders/nq/wq3c5mv50nb4fnymxft9ggv40000gn/T/tmp.pqv3hiP57n/internal/service/webhook/webhook.go","line":89},"msg":"failed to parse webhook payload","error":"invalid character 'x' looking for beginning of value"}
{"time":"2026-02-15T21:32:46.459498-08:00","level":"INFO","source":{"function":"git.eeqj.de/sneak/upaas/internal/service/webhook.(*Service).HandleWebhook","file":"/var/folders/nq/wq3c5mv50nb4fnymxft9ggv40000gn/T/tmp.pqv3hiP57n/internal/service/webhook/webhook.go","line":117},"msg":"webhook event recorded","app":"oversize-test-app","branch":"","matched":false,"commit":""}
--- PASS: TestHandleWebhookRejectsOversizedBody (0.06s)
--- PASS: TestHandleWebhookProcessesValidWebhook (0.15s)
--- PASS: TestHandleSetupPOSTCreatesUserAndRedirects (0.17s)
--- PASS: TestHandleLoginPOSTAuthenticatesValidCredentials (0.32s)
--- PASS: TestHandleLoginPOSTRejectsInvalidCredentials (0.33s)
PASS
coverage: 20.9% of statements
ok  	git.eeqj.de/sneak/upaas/internal/handlers	(cached)	coverage: 20.9% of statements
	git.eeqj.de/sneak/upaas/internal/healthcheck		coverage: 0.0% of statements
	git.eeqj.de/sneak/upaas/internal/logger		coverage: 0.0% of statements
--- PASS: TestLoginRateLimitAllowsUpToBurst (0.00s)
--- PASS: TestLoginRateLimitIsolatesIPs (0.00s)
--- PASS: TestLoginRateLimitReturns429Body (0.00s)
--- PASS: TestIPLimiterEvictsStaleEntries (0.00s)
--- PASS: TestRealIP (0.00s)
    --- PASS: TestRealIP/X-Real-IP_takes_priority (0.00s)
    --- PASS: TestRealIP/X-Forwarded-For_used_when_no_X-Real-IP (0.00s)
    --- PASS: TestRealIP/X-Forwarded-For_single_IP (0.00s)
    --- PASS: TestRealIP/falls_back_to_RemoteAddr (0.00s)
    --- PASS: TestRealIP/RemoteAddr_without_port (0.00s)
    --- PASS: TestRealIP/X-Real-IP_with_whitespace (0.00s)
    --- PASS: TestRealIP/X-Forwarded-For_with_whitespace (0.00s)
    --- PASS: TestRealIP/empty_X-Real-IP_falls_through_to_XFF (0.00s)
PASS
coverage: 47.1% of statements
ok  	git.eeqj.de/sneak/upaas/internal/middleware	(cached)	coverage: 47.1% of statements
--- PASS: TestDeploymentCreateAndFind (0.06s)
--- PASS: TestAppGetWebhookEvents (0.06s)
--- PASS: TestDeploymentFindByAppID (0.07s)
--- PASS: TestDeploymentMarkFinished (0.07s)
--- PASS: TestAppUpdate (0.06s)
--- PASS: TestAppGetEnvVars (0.07s)
--- PASS: TestAppFindByWebhookSecret (0.07s)
--- PASS: TestAppDelete (0.07s)
--- PASS: TestVolumeCRUD (0.00s)
    --- PASS: TestVolumeCRUD/creates_and_finds_volumes (0.07s)
--- PASS: TestEnvVarCRUD (0.00s)
    --- PASS: TestEnvVarCRUD/creates_and_finds_env_vars (0.06s)
    --- PASS: TestEnvVarCRUD/deletes_env_var (0.07s)
--- PASS: TestLabelCRUD (0.00s)
    --- PASS: TestLabelCRUD/creates_and_finds_labels (0.07s)
--- PASS: TestCascadeDelete (0.00s)
    --- PASS: TestCascadeDelete/deleting_app_cascades_to_related_records (0.07s)
--- PASS: TestAppGetDeployments (0.07s)
--- PASS: TestDeploymentFindLatest (0.07s)
--- PASS: TestAppGetLabels (0.07s)
--- PASS: TestAppCreateAndFind (0.07s)
--- PASS: TestDeploymentAppendLog (0.07s)
--- PASS: TestUserCreateAndFind (0.08s)
--- PASS: TestAllApps (0.00s)
    --- PASS: TestAllApps/returns_empty_list_when_no_apps (0.07s)
    --- PASS: TestAllApps/returns_apps_ordered_by_name (0.07s)
--- PASS: TestWebhookEventCRUD (0.00s)
    --- PASS: TestWebhookEventCRUD/creates_and_finds_webhook_events (0.07s)
--- PASS: TestUserUpdate (0.08s)
--- PASS: TestUserFindByUsernameNotFound (0.08s)
--- PASS: TestUserDelete (0.08s)
--- PASS: TestUserExists (0.00s)
    --- PASS: TestUserExists/returns_true_when_user_exists (0.05s)
    --- PASS: TestUserExists/returns_false_when_no_users (0.07s)
--- PASS: TestUserFindByUsername (0.08s)
--- PASS: TestAppGetVolumes (0.08s)
PASS
coverage: 56.3% of statements
ok  	git.eeqj.de/sneak/upaas/internal/models	(cached)	coverage: 56.3% of statements
	git.eeqj.de/sneak/upaas/internal/server		coverage: 0.0% of statements
--- PASS: TestEnvVarsDelete (0.07s)
--- PASS: TestUpdateApp (0.00s)
    --- PASS: TestUpdateApp/updates_app_fields (0.06s)
    --- PASS: TestUpdateApp/clears_optional_fields_when_empty (0.08s)
--- PASS: TestGetApp (0.00s)
    --- PASS: TestGetApp/returns_nil_for_non-existent_app (0.08s)
    --- PASS: TestGetApp/finds_existing_app (0.08s)
--- PASS: TestCreateAppOptionalFields (0.09s)
--- PASS: TestVolumesDelete (0.11s)
--- PASS: TestUpdateAppStatus (0.01s)
    --- PASS: TestUpdateAppStatus/updates_app_status (0.10s)
--- PASS: TestVolumesAddAndRetrieve (0.11s)
--- PASS: TestCreateAppDefaults (0.11s)
--- PASS: TestEnvVarsAddAndRetrieve (0.10s)
--- PASS: TestCreateAppWithGeneratedKeys (0.10s)
--- PASS: TestGetAppByWebhookSecret (0.02s)
    --- PASS: TestGetAppByWebhookSecret/returns_nil_for_invalid_secret (0.10s)
    --- PASS: TestGetAppByWebhookSecret/finds_app_by_webhook_secret (0.10s)
--- PASS: TestDeleteApp (0.00s)
    --- PASS: TestDeleteApp/deletes_app_and_returns_nil_on_lookup (0.11s)
--- PASS: TestLabels (0.02s)
    --- PASS: TestLabels/adds_and_retrieves_labels (0.09s)
    --- PASS: TestLabels/deletes_label (0.10s)
--- PASS: TestListApps (0.00s)
    --- PASS: TestListApps/returns_empty_list_when_no_apps (0.09s)
    --- PASS: TestListApps/returns_all_apps_ordered_by_name (0.11s)
PASS
coverage: 82.8% of statements
ok  	git.eeqj.de/sneak/upaas/internal/service/app	(cached)	coverage: 82.8% of statements
--- PASS: TestIsSetupRequired (0.00s)
    --- PASS: TestIsSetupRequired/returns_true_when_no_users_exist (0.04s)
--- PASS: TestCreateUser (0.00s)
    --- PASS: TestCreateUser/creates_user_successfully (0.29s)
    --- PASS: TestCreateUser/rejects_duplicate_user (0.30s)
--- PASS: TestHashPassword (0.00s)
    --- PASS: TestHashPassword/hashes_password_successfully (0.30s)
    --- PASS: TestHashPassword/produces_different_hashes_for_same_password (0.48s)
--- PASS: TestVerifyPassword (0.00s)
    --- PASS: TestVerifyPassword/rejects_invalid_hash_format (0.04s)
    --- PASS: TestVerifyPassword/verifies_correct_password (0.47s)
    --- PASS: TestVerifyPassword/rejects_empty_password (0.49s)
    --- PASS: TestVerifyPassword/rejects_incorrect_password (0.50s)
--- PASS: TestSessionCookieSecureFlag (0.00s)
    --- PASS: TestSessionCookieSecureFlag/secure_flag_is_true_when_debug_is_false (0.52s)
--- PASS: TestAuthenticate (0.00s)
    --- PASS: TestAuthenticate/rejects_unknown_user (0.05s)
    --- PASS: TestAuthenticate/authenticates_valid_credentials (0.51s)
    --- PASS: TestAuthenticate/rejects_invalid_password (0.53s)
PASS
coverage: 64.2% of statements
ok  	git.eeqj.de/sneak/upaas/internal/service/auth	(cached)	coverage: 64.2% of statements
	git.eeqj.de/sneak/upaas/internal/service/deploy		coverage: 0.0% of statements
	git.eeqj.de/sneak/upaas/internal/service/notify		coverage: 0.0% of statements
--- PASS: TestGiteaPushPayloadParsing (0.00s)
    --- PASS: TestGiteaPushPayloadParsing/parses_full_payload (0.00s)
{"time":"2026-02-15T21:32:46.838131-08:00","level":"INFO","source":{"function":"git.eeqj.de/sneak/upaas/internal/service/webhook.(*Service).HandleWebhook","file":"/var/folders/nq/wq3c5mv50nb4fnymxft9ggv40000gn/T/tmp.pqv3hiP57n/internal/service/webhook/webhook.go","line":117},"msg":"webhook event recorded","app":"test-app","branch":"","matched":true,"commit":""}
{"time":"2026-02-15T21:32:46.840113-08:00","level":"INFO","source":{"function":"git.eeqj.de/sneak/upaas/internal/service/webhook.(*Service).HandleWebhook","file":"/var/folders/nq/wq3c5mv50nb4fnymxft9ggv40000gn/T/tmp.pqv3hiP57n/internal/service/webhook/webhook.go","line":117},"msg":"webhook event recorded","app":"test-app","branch":"","matched":false,"commit":""}
--- PASS: TestHandleWebhookEmptyPayload (0.04s)
{"time":"2026-02-15T21:32:46.841099-08:00","level":"WARN","source":{"function":"git.eeqj.de/sneak/upaas/internal/service/webhook.(*Service).HandleWebhook","file":"/var/folders/nq/wq3c5mv50nb4fnymxft9ggv40000gn/T/tmp.pqv3hiP57n/internal/service/webhook/webhook.go","line":89},"msg":"failed to parse webhook payload","error":"invalid character 'i' looking for beginning of object key string"}
{"time":"2026-02-15T21:32:46.841359-08:00","level":"INFO","source":{"function":"git.eeqj.de/sneak/upaas/internal/service/webhook.(*Service).HandleWebhook","file":"/var/folders/nq/wq3c5mv50nb4fnymxft9ggv40000gn/T/tmp.pqv3hiP57n/internal/service/webhook/webhook.go","line":117},"msg":"webhook event recorded","app":"test-app","branch":"","matched":false,"commit":""}
{"time":"2026-02-15T21:32:46.841717-08:00","level":"INFO","source":{"function":"git.eeqj.de/sneak/upaas/internal/service/webhook.(*Service).HandleWebhook","file":"/var/folders/nq/wq3c5mv50nb4fnymxft9ggv40000gn/T/tmp.pqv3hiP57n/internal/service/webhook/webhook.go","line":117},"msg":"webhook event recorded","app":"test-app","branch":"main","matched":true,"commit":""}
--- PASS: TestHandleWebhookInvalidJSON (0.04s)
{"time":"2026-02-15T21:32:46.843436-08:00","level":"INFO","source":{"function":"git.eeqj.de/sneak/upaas/internal/service/webhook.(*Service).HandleWebhook","file":"/var/folders/nq/wq3c5mv50nb4fnymxft9ggv40000gn/T/tmp.pqv3hiP57n/internal/service/webhook/webhook.go","line":117},"msg":"webhook event recorded","app":"test-app","branch":"","matched":true,"commit":""}
{"time":"2026-02-15T21:32:46.848471-08:00","level":"INFO","source":{"function":"git.eeqj.de/sneak/upaas/internal/service/webhook.(*Service).HandleWebhook","file":"/var/folders/nq/wq3c5mv50nb4fnymxft9ggv40000gn/T/tmp.pqv3hiP57n/internal/service/webhook/webhook.go","line":117},"msg":"webhook event recorded","app":"test-app","branch":"develop","matched":false,"commit":"def789ghi012"}
{"time":"2026-02-15T21:32:46.848479-08:00","level":"INFO","source":{"function":"git.eeqj.de/sneak/upaas/internal/service/webhook.(*Service).HandleWebhook","file":"/var/folders/nq/wq3c5mv50nb4fnymxft9ggv40000gn/T/tmp.pqv3hiP57n/internal/service/webhook/webhook.go","line":117},"msg":"webhook event recorded","app":"test-app","branch":"develop","matched":true,"commit":""}
{"time":"2026-02-15T21:32:46.849413-08:00","level":"INFO","source":{"function":"git.eeqj.de/sneak/upaas/internal/service/webhook.(*Service).HandleWebhook","file":"/var/folders/nq/wq3c5mv50nb4fnymxft9ggv40000gn/T/tmp.pqv3hiP57n/internal/service/webhook/webhook.go","line":117},"msg":"webhook event recorded","app":"test-app","branch":"main","matched":true,"commit":""}
--- PASS: TestHandleWebhookNonMatchingBranch (0.05s)
{"time":"2026-02-15T21:32:46.849608-08:00","level":"INFO","source":{"function":"git.eeqj.de/sneak/upaas/internal/service/webhook.(*Service).HandleWebhook","file":"/var/folders/nq/wq3c5mv50nb4fnymxft9ggv40000gn/T/tmp.pqv3hiP57n/internal/service/webhook/webhook.go","line":117},"msg":"webhook event recorded","app":"test-app","branch":"feature/new-feature","matched":true,"commit":""}
--- PASS: TestSetupTestService (0.00s)
    --- PASS: TestSetupTestService/creates_working_test_service (0.04s)
{"time":"2026-02-15T21:32:46.851331-08:00","level":"INFO","source":{"function":"git.eeqj.de/sneak/upaas/internal/service/webhook.(*Service).HandleWebhook","file":"/var/folders/nq/wq3c5mv50nb4fnymxft9ggv40000gn/T/tmp.pqv3hiP57n/internal/service/webhook/webhook.go","line":117},"msg":"webhook event recorded","app":"test-app","branch":"main","matched":true,"commit":"abc123def456"}
--- PASS: TestExtractBranch (0.00s)
    --- PASS: TestExtractBranch/handles_empty_ref (0.13s)
    --- PASS: TestExtractBranch/handles_partial_prefix (0.14s)
    --- PASS: TestExtractBranch/extracts_main_branch (0.14s)
    --- PASS: TestExtractBranch/extracts_develop_branch (0.14s)
    --- PASS: TestExtractBranch/returns_raw_ref_if_no_prefix (0.15s)
    --- PASS: TestExtractBranch/extracts_feature_branch (0.14s)
--- PASS: TestHandleWebhookMatchingBranch (0.15s)
PASS
coverage: 93.3% of statements
ok  	git.eeqj.de/sneak/upaas/internal/service/webhook	(cached)	coverage: 93.3% of statements
--- PASS: TestValidatePrivateKey (0.00s)
    --- PASS: TestValidatePrivateKey/rejects_invalid_key (0.00s)
    --- PASS: TestValidatePrivateKey/rejects_empty_key (0.00s)
    --- PASS: TestValidatePrivateKey/validates_generated_key (0.01s)
--- PASS: TestGenerateKeyPair (0.00s)
    --- PASS: TestGenerateKeyPair/generates_valid_key_pair (0.01s)
    --- PASS: TestGenerateKeyPair/generates_unique_keys_each_time (0.01s)
PASS
coverage: 78.6% of statements
ok  	git.eeqj.de/sneak/upaas/internal/ssh	(cached)	coverage: 78.6% of statements
	git.eeqj.de/sneak/upaas/templates		coverage: 0.0% of statements
## Test Results All tests pass: ``` git.eeqj.de/sneak/upaas/cmd/upaasd coverage: 0.0% of statements git.eeqj.de/sneak/upaas/internal/config coverage: 0.0% of statements --- PASS: TestHashWebhookSecret (0.00s) PASS coverage: 1.6% of statements ok git.eeqj.de/sneak/upaas/internal/database (cached) coverage: 1.6% of statements --- PASS: TestValidBranchRegex (0.00s) --- PASS: TestValidCommitSHARegex (0.00s) --- PASS: TestCloneRepoRejectsInjection (0.00s) --- PASS: TestCloneRepoRejectsInjection/shell_injection_in_branch (0.00s) --- PASS: TestCloneRepoRejectsInjection/command_substitution_in_branch (0.00s) --- PASS: TestCloneRepoRejectsInjection/backtick_injection_in_branch (0.00s) --- PASS: TestCloneRepoRejectsInjection/injection_in_commitSHA (0.00s) --- PASS: TestCloneRepoRejectsInjection/short_SHA_rejected (0.00s) --- PASS: TestCloneRepoRejectsInjection/valid_inputs_pass_validation_(hit_NotConnected) (0.00s) --- PASS: TestCloneRepoRejectsInjection/valid_branch_no_SHA_passes_validation_(hit_NotConnected) (0.00s) PASS coverage: 2.8% of statements ok git.eeqj.de/sneak/upaas/internal/docker (cached) coverage: 2.8% of statements git.eeqj.de/sneak/upaas/internal/globals coverage: 0.0% of statements --- PASS: TestHandleSetupPOSTRejectsMismatchedPasswords (0.05s) {"time":"2026-02-15T21:32:46.448831-08:00","level":"INFO","source":{"function":"git.eeqj.de/sneak/upaas/internal/service/webhook.(*Service).HandleWebhook","file":"/var/folders/nq/wq3c5mv50nb4fnymxft9ggv40000gn/T/tmp.pqv3hiP57n/internal/service/webhook/webhook.go","line":117},"msg":"webhook event recorded","app":"webhook-test-app","branch":"main","matched":true,"commit":"abc123"} --- PASS: TestHandleSetupPOSTRejectsShortPassword (0.05s) --- PASS: TestHandleSetupPOSTRejectsEmptyUsername (0.05s) --- PASS: TestHandleWebhookReturns404ForUnknownSecret (0.05s) --- PASS: TestDeleteVolumeOwnershipVerification (0.05s) --- PASS: TestHandleHealthCheck (0.00s) --- PASS: TestHandleHealthCheck/returns_health_check_response (0.05s) --- PASS: TestDeletePortOwnershipVerification (0.05s) --- PASS: TestHandleSetupGET (0.00s) --- PASS: TestHandleSetupGET/renders_setup_page (0.05s) --- PASS: TestHandleAppNew (0.00s) --- PASS: TestHandleAppNew/renders_new_app_form (0.05s) --- PASS: TestHandleLoginGET (0.00s) --- PASS: TestHandleLoginGET/renders_login_page (0.05s) --- PASS: TestHandleDashboard (0.00s) --- PASS: TestHandleDashboard/renders_dashboard_with_app_list (0.05s) --- PASS: TestDeleteEnvVarOwnershipVerification (0.05s) --- PASS: TestDeleteLabelOwnershipVerification (0.06s) {"time":"2026-02-15T21:32:46.454183-08:00","level":"WARN","source":{"function":"git.eeqj.de/sneak/upaas/internal/service/webhook.(*Service).HandleWebhook","file":"/var/folders/nq/wq3c5mv50nb4fnymxft9ggv40000gn/T/tmp.pqv3hiP57n/internal/service/webhook/webhook.go","line":89},"msg":"failed to parse webhook payload","error":"invalid character 'x' looking for beginning of value"} {"time":"2026-02-15T21:32:46.459498-08:00","level":"INFO","source":{"function":"git.eeqj.de/sneak/upaas/internal/service/webhook.(*Service).HandleWebhook","file":"/var/folders/nq/wq3c5mv50nb4fnymxft9ggv40000gn/T/tmp.pqv3hiP57n/internal/service/webhook/webhook.go","line":117},"msg":"webhook event recorded","app":"oversize-test-app","branch":"","matched":false,"commit":""} --- PASS: TestHandleWebhookRejectsOversizedBody (0.06s) --- PASS: TestHandleWebhookProcessesValidWebhook (0.15s) --- PASS: TestHandleSetupPOSTCreatesUserAndRedirects (0.17s) --- PASS: TestHandleLoginPOSTAuthenticatesValidCredentials (0.32s) --- PASS: TestHandleLoginPOSTRejectsInvalidCredentials (0.33s) PASS coverage: 20.9% of statements ok git.eeqj.de/sneak/upaas/internal/handlers (cached) coverage: 20.9% of statements git.eeqj.de/sneak/upaas/internal/healthcheck coverage: 0.0% of statements git.eeqj.de/sneak/upaas/internal/logger coverage: 0.0% of statements --- PASS: TestLoginRateLimitAllowsUpToBurst (0.00s) --- PASS: TestLoginRateLimitIsolatesIPs (0.00s) --- PASS: TestLoginRateLimitReturns429Body (0.00s) --- PASS: TestIPLimiterEvictsStaleEntries (0.00s) --- PASS: TestRealIP (0.00s) --- PASS: TestRealIP/X-Real-IP_takes_priority (0.00s) --- PASS: TestRealIP/X-Forwarded-For_used_when_no_X-Real-IP (0.00s) --- PASS: TestRealIP/X-Forwarded-For_single_IP (0.00s) --- PASS: TestRealIP/falls_back_to_RemoteAddr (0.00s) --- PASS: TestRealIP/RemoteAddr_without_port (0.00s) --- PASS: TestRealIP/X-Real-IP_with_whitespace (0.00s) --- PASS: TestRealIP/X-Forwarded-For_with_whitespace (0.00s) --- PASS: TestRealIP/empty_X-Real-IP_falls_through_to_XFF (0.00s) PASS coverage: 47.1% of statements ok git.eeqj.de/sneak/upaas/internal/middleware (cached) coverage: 47.1% of statements --- PASS: TestDeploymentCreateAndFind (0.06s) --- PASS: TestAppGetWebhookEvents (0.06s) --- PASS: TestDeploymentFindByAppID (0.07s) --- PASS: TestDeploymentMarkFinished (0.07s) --- PASS: TestAppUpdate (0.06s) --- PASS: TestAppGetEnvVars (0.07s) --- PASS: TestAppFindByWebhookSecret (0.07s) --- PASS: TestAppDelete (0.07s) --- PASS: TestVolumeCRUD (0.00s) --- PASS: TestVolumeCRUD/creates_and_finds_volumes (0.07s) --- PASS: TestEnvVarCRUD (0.00s) --- PASS: TestEnvVarCRUD/creates_and_finds_env_vars (0.06s) --- PASS: TestEnvVarCRUD/deletes_env_var (0.07s) --- PASS: TestLabelCRUD (0.00s) --- PASS: TestLabelCRUD/creates_and_finds_labels (0.07s) --- PASS: TestCascadeDelete (0.00s) --- PASS: TestCascadeDelete/deleting_app_cascades_to_related_records (0.07s) --- PASS: TestAppGetDeployments (0.07s) --- PASS: TestDeploymentFindLatest (0.07s) --- PASS: TestAppGetLabels (0.07s) --- PASS: TestAppCreateAndFind (0.07s) --- PASS: TestDeploymentAppendLog (0.07s) --- PASS: TestUserCreateAndFind (0.08s) --- PASS: TestAllApps (0.00s) --- PASS: TestAllApps/returns_empty_list_when_no_apps (0.07s) --- PASS: TestAllApps/returns_apps_ordered_by_name (0.07s) --- PASS: TestWebhookEventCRUD (0.00s) --- PASS: TestWebhookEventCRUD/creates_and_finds_webhook_events (0.07s) --- PASS: TestUserUpdate (0.08s) --- PASS: TestUserFindByUsernameNotFound (0.08s) --- PASS: TestUserDelete (0.08s) --- PASS: TestUserExists (0.00s) --- PASS: TestUserExists/returns_true_when_user_exists (0.05s) --- PASS: TestUserExists/returns_false_when_no_users (0.07s) --- PASS: TestUserFindByUsername (0.08s) --- PASS: TestAppGetVolumes (0.08s) PASS coverage: 56.3% of statements ok git.eeqj.de/sneak/upaas/internal/models (cached) coverage: 56.3% of statements git.eeqj.de/sneak/upaas/internal/server coverage: 0.0% of statements --- PASS: TestEnvVarsDelete (0.07s) --- PASS: TestUpdateApp (0.00s) --- PASS: TestUpdateApp/updates_app_fields (0.06s) --- PASS: TestUpdateApp/clears_optional_fields_when_empty (0.08s) --- PASS: TestGetApp (0.00s) --- PASS: TestGetApp/returns_nil_for_non-existent_app (0.08s) --- PASS: TestGetApp/finds_existing_app (0.08s) --- PASS: TestCreateAppOptionalFields (0.09s) --- PASS: TestVolumesDelete (0.11s) --- PASS: TestUpdateAppStatus (0.01s) --- PASS: TestUpdateAppStatus/updates_app_status (0.10s) --- PASS: TestVolumesAddAndRetrieve (0.11s) --- PASS: TestCreateAppDefaults (0.11s) --- PASS: TestEnvVarsAddAndRetrieve (0.10s) --- PASS: TestCreateAppWithGeneratedKeys (0.10s) --- PASS: TestGetAppByWebhookSecret (0.02s) --- PASS: TestGetAppByWebhookSecret/returns_nil_for_invalid_secret (0.10s) --- PASS: TestGetAppByWebhookSecret/finds_app_by_webhook_secret (0.10s) --- PASS: TestDeleteApp (0.00s) --- PASS: TestDeleteApp/deletes_app_and_returns_nil_on_lookup (0.11s) --- PASS: TestLabels (0.02s) --- PASS: TestLabels/adds_and_retrieves_labels (0.09s) --- PASS: TestLabels/deletes_label (0.10s) --- PASS: TestListApps (0.00s) --- PASS: TestListApps/returns_empty_list_when_no_apps (0.09s) --- PASS: TestListApps/returns_all_apps_ordered_by_name (0.11s) PASS coverage: 82.8% of statements ok git.eeqj.de/sneak/upaas/internal/service/app (cached) coverage: 82.8% of statements --- PASS: TestIsSetupRequired (0.00s) --- PASS: TestIsSetupRequired/returns_true_when_no_users_exist (0.04s) --- PASS: TestCreateUser (0.00s) --- PASS: TestCreateUser/creates_user_successfully (0.29s) --- PASS: TestCreateUser/rejects_duplicate_user (0.30s) --- PASS: TestHashPassword (0.00s) --- PASS: TestHashPassword/hashes_password_successfully (0.30s) --- PASS: TestHashPassword/produces_different_hashes_for_same_password (0.48s) --- PASS: TestVerifyPassword (0.00s) --- PASS: TestVerifyPassword/rejects_invalid_hash_format (0.04s) --- PASS: TestVerifyPassword/verifies_correct_password (0.47s) --- PASS: TestVerifyPassword/rejects_empty_password (0.49s) --- PASS: TestVerifyPassword/rejects_incorrect_password (0.50s) --- PASS: TestSessionCookieSecureFlag (0.00s) --- PASS: TestSessionCookieSecureFlag/secure_flag_is_true_when_debug_is_false (0.52s) --- PASS: TestAuthenticate (0.00s) --- PASS: TestAuthenticate/rejects_unknown_user (0.05s) --- PASS: TestAuthenticate/authenticates_valid_credentials (0.51s) --- PASS: TestAuthenticate/rejects_invalid_password (0.53s) PASS coverage: 64.2% of statements ok git.eeqj.de/sneak/upaas/internal/service/auth (cached) coverage: 64.2% of statements git.eeqj.de/sneak/upaas/internal/service/deploy coverage: 0.0% of statements git.eeqj.de/sneak/upaas/internal/service/notify coverage: 0.0% of statements --- PASS: TestGiteaPushPayloadParsing (0.00s) --- PASS: TestGiteaPushPayloadParsing/parses_full_payload (0.00s) {"time":"2026-02-15T21:32:46.838131-08:00","level":"INFO","source":{"function":"git.eeqj.de/sneak/upaas/internal/service/webhook.(*Service).HandleWebhook","file":"/var/folders/nq/wq3c5mv50nb4fnymxft9ggv40000gn/T/tmp.pqv3hiP57n/internal/service/webhook/webhook.go","line":117},"msg":"webhook event recorded","app":"test-app","branch":"","matched":true,"commit":""} {"time":"2026-02-15T21:32:46.840113-08:00","level":"INFO","source":{"function":"git.eeqj.de/sneak/upaas/internal/service/webhook.(*Service).HandleWebhook","file":"/var/folders/nq/wq3c5mv50nb4fnymxft9ggv40000gn/T/tmp.pqv3hiP57n/internal/service/webhook/webhook.go","line":117},"msg":"webhook event recorded","app":"test-app","branch":"","matched":false,"commit":""} --- PASS: TestHandleWebhookEmptyPayload (0.04s) {"time":"2026-02-15T21:32:46.841099-08:00","level":"WARN","source":{"function":"git.eeqj.de/sneak/upaas/internal/service/webhook.(*Service).HandleWebhook","file":"/var/folders/nq/wq3c5mv50nb4fnymxft9ggv40000gn/T/tmp.pqv3hiP57n/internal/service/webhook/webhook.go","line":89},"msg":"failed to parse webhook payload","error":"invalid character 'i' looking for beginning of object key string"} {"time":"2026-02-15T21:32:46.841359-08:00","level":"INFO","source":{"function":"git.eeqj.de/sneak/upaas/internal/service/webhook.(*Service).HandleWebhook","file":"/var/folders/nq/wq3c5mv50nb4fnymxft9ggv40000gn/T/tmp.pqv3hiP57n/internal/service/webhook/webhook.go","line":117},"msg":"webhook event recorded","app":"test-app","branch":"","matched":false,"commit":""} {"time":"2026-02-15T21:32:46.841717-08:00","level":"INFO","source":{"function":"git.eeqj.de/sneak/upaas/internal/service/webhook.(*Service).HandleWebhook","file":"/var/folders/nq/wq3c5mv50nb4fnymxft9ggv40000gn/T/tmp.pqv3hiP57n/internal/service/webhook/webhook.go","line":117},"msg":"webhook event recorded","app":"test-app","branch":"main","matched":true,"commit":""} --- PASS: TestHandleWebhookInvalidJSON (0.04s) {"time":"2026-02-15T21:32:46.843436-08:00","level":"INFO","source":{"function":"git.eeqj.de/sneak/upaas/internal/service/webhook.(*Service).HandleWebhook","file":"/var/folders/nq/wq3c5mv50nb4fnymxft9ggv40000gn/T/tmp.pqv3hiP57n/internal/service/webhook/webhook.go","line":117},"msg":"webhook event recorded","app":"test-app","branch":"","matched":true,"commit":""} {"time":"2026-02-15T21:32:46.848471-08:00","level":"INFO","source":{"function":"git.eeqj.de/sneak/upaas/internal/service/webhook.(*Service).HandleWebhook","file":"/var/folders/nq/wq3c5mv50nb4fnymxft9ggv40000gn/T/tmp.pqv3hiP57n/internal/service/webhook/webhook.go","line":117},"msg":"webhook event recorded","app":"test-app","branch":"develop","matched":false,"commit":"def789ghi012"} {"time":"2026-02-15T21:32:46.848479-08:00","level":"INFO","source":{"function":"git.eeqj.de/sneak/upaas/internal/service/webhook.(*Service).HandleWebhook","file":"/var/folders/nq/wq3c5mv50nb4fnymxft9ggv40000gn/T/tmp.pqv3hiP57n/internal/service/webhook/webhook.go","line":117},"msg":"webhook event recorded","app":"test-app","branch":"develop","matched":true,"commit":""} {"time":"2026-02-15T21:32:46.849413-08:00","level":"INFO","source":{"function":"git.eeqj.de/sneak/upaas/internal/service/webhook.(*Service).HandleWebhook","file":"/var/folders/nq/wq3c5mv50nb4fnymxft9ggv40000gn/T/tmp.pqv3hiP57n/internal/service/webhook/webhook.go","line":117},"msg":"webhook event recorded","app":"test-app","branch":"main","matched":true,"commit":""} --- PASS: TestHandleWebhookNonMatchingBranch (0.05s) {"time":"2026-02-15T21:32:46.849608-08:00","level":"INFO","source":{"function":"git.eeqj.de/sneak/upaas/internal/service/webhook.(*Service).HandleWebhook","file":"/var/folders/nq/wq3c5mv50nb4fnymxft9ggv40000gn/T/tmp.pqv3hiP57n/internal/service/webhook/webhook.go","line":117},"msg":"webhook event recorded","app":"test-app","branch":"feature/new-feature","matched":true,"commit":""} --- PASS: TestSetupTestService (0.00s) --- PASS: TestSetupTestService/creates_working_test_service (0.04s) {"time":"2026-02-15T21:32:46.851331-08:00","level":"INFO","source":{"function":"git.eeqj.de/sneak/upaas/internal/service/webhook.(*Service).HandleWebhook","file":"/var/folders/nq/wq3c5mv50nb4fnymxft9ggv40000gn/T/tmp.pqv3hiP57n/internal/service/webhook/webhook.go","line":117},"msg":"webhook event recorded","app":"test-app","branch":"main","matched":true,"commit":"abc123def456"} --- PASS: TestExtractBranch (0.00s) --- PASS: TestExtractBranch/handles_empty_ref (0.13s) --- PASS: TestExtractBranch/handles_partial_prefix (0.14s) --- PASS: TestExtractBranch/extracts_main_branch (0.14s) --- PASS: TestExtractBranch/extracts_develop_branch (0.14s) --- PASS: TestExtractBranch/returns_raw_ref_if_no_prefix (0.15s) --- PASS: TestExtractBranch/extracts_feature_branch (0.14s) --- PASS: TestHandleWebhookMatchingBranch (0.15s) PASS coverage: 93.3% of statements ok git.eeqj.de/sneak/upaas/internal/service/webhook (cached) coverage: 93.3% of statements --- PASS: TestValidatePrivateKey (0.00s) --- PASS: TestValidatePrivateKey/rejects_invalid_key (0.00s) --- PASS: TestValidatePrivateKey/rejects_empty_key (0.00s) --- PASS: TestValidatePrivateKey/validates_generated_key (0.01s) --- PASS: TestGenerateKeyPair (0.00s) --- PASS: TestGenerateKeyPair/generates_valid_key_pair (0.01s) --- PASS: TestGenerateKeyPair/generates_unique_keys_each_time (0.01s) PASS coverage: 78.6% of statements ok git.eeqj.de/sneak/upaas/internal/ssh (cached) coverage: 78.6% of statements git.eeqj.de/sneak/upaas/templates coverage: 0.0% of statements ```
sneak merged commit ef271d2da9 into main 2026-02-16 06:38:30 +01:00
Sign in to join this conversation.
No reviewers
No Milestone
No project
No Assignees
1 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: sneak/upaas#29
No description provided.