diff --git a/Reports/Ghaymah_CLI_v2_Binary_Audit_Report.md b/Reports/Ghaymah_CLI_v2_Binary_Audit_Report.md index 93cf716..a7b4096 100644 --- a/Reports/Ghaymah_CLI_v2_Binary_Audit_Report.md +++ b/Reports/Ghaymah_CLI_v2_Binary_Audit_Report.md @@ -316,3 +316,767 @@ user_id=d62f9886-fced-4cf2-98e4-5b62000d4f03 *The output definitively proves input validation bypasses, symlink traversal, and proxy interception capabilities.* ===================================================================== ``` + +--- + +## πŸ”§ Drop-In Go Remediation Patches + +> **Usage:** Each patch below is a self-contained Go file that can be added directly to the corresponding package. Import and call the exported functions from the existing command handlers to enforce the fix. + +--- + +### PATCH-01: Client-Side Input Validation (Fixes NEW-VULN-01 & NEW-VULN-02) + +**Target:** `pkg/validation/input.go` *(new file)* + +```go +// Package validation provides client-side input sanitization +// for the Ghaymah CLI before values are transmitted to the backend API. +package validation + +import ( + "fmt" + "regexp" +) + +// ValidProjectName enforces DNS-compatible naming: +// - Only lowercase alphanumeric characters and hyphens +// - Must start and end with an alphanumeric character +// - Length between 3 and 63 characters (RFC 1035) +var ValidProjectName = regexp.MustCompile(`^[a-z0-9]([a-z0-9-]{1,61}[a-z0-9])?$`) + +// ValidateName checks that a project/app name is safe for backend consumption. +// It rejects path traversals, XSS payloads, template injections, and oversized strings. +func ValidateName(name string) error { + if len(name) < 3 { + return fmt.Errorf("name %q is too short: minimum 3 characters required", name) + } + if len(name) > 63 { + return fmt.Errorf("name %q is too long (%d chars): maximum 63 characters allowed", name, len(name)) + } + if !ValidProjectName.MatchString(name) { + return fmt.Errorf( + "name %q contains invalid characters: only lowercase letters (a-z), "+ + "digits (0-9), and hyphens (-) are allowed; must start and end with "+ + "an alphanumeric character", + name, + ) + } + return nil +} + +// ValidatePort checks that a port number falls within the valid TCP range (1–65535). +func ValidatePort(port int) error { + if port < 1 || port > 65535 { + return fmt.Errorf( + "port %d is out of range: must be between 1 and 65535 (valid TCP port)", + port, + ) + } + return nil +} + +// ValidateDomain checks that a domain string is safe and well-formed. +var ValidDomain = regexp.MustCompile(`^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}$`) + +func ValidateDomainName(domain string) error { + if len(domain) > 253 { + return fmt.Errorf("domain %q exceeds maximum length of 253 characters", domain) + } + if !ValidDomain.MatchString(domain) { + return fmt.Errorf("domain %q is not a valid hostname", domain) + } + return nil +} +``` + +**Integration point in `cmd/deploy.go`:** +```go +// Add at the top of the deploy command's RunE function: +if err := validation.ValidatePort(port); err != nil { + return err +} +if name != "" { + if err := validation.ValidateName(name); err != nil { + return err + } +} +if domain != "" { + if err := validation.ValidateDomainName(domain); err != nil { + return err + } +} +``` + +--- + +### PATCH-02: Secure Token Storage via OS Keyring (Fixes NEW-VULN-03) + +**Target:** `pkg/auth/securestore.go` *(new file)* +**Dependency:** `go get github.com/zalando/go-keyring` + +```go +// Package auth provides secure credential storage using the native OS keychain +// instead of plaintext JSON files on disk. +package auth + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "runtime" + + "github.com/zalando/go-keyring" +) + +const ( + // ServiceName identifies the credential entry in the OS keyring. + ServiceName = "ghaymah-cli" + // TokenKey is the keyring key under which the serialized token is stored. + TokenKey = "auth-token" +) + +// StoredToken mirrors the existing nhost.StoredToken structure. +type StoredToken struct { + AccessToken string `json:"accessToken"` + RefreshToken string `json:"refreshToken"` + RefreshTokenID string `json:"refreshTokenId,omitempty"` + User struct { + ID string `json:"id"` + Email string `json:"email"` + } `json:"user"` +} + +// SaveTokenSecurely stores the auth token in the OS-native credential vault. +// Falls back to encrypted file storage if the keyring is unavailable (e.g., headless CI). +func SaveTokenSecurely(token *StoredToken) error { + data, err := json.Marshal(token) + if err != nil { + return fmt.Errorf("failed to serialize token: %w", err) + } + + if err := keyring.Set(ServiceName, TokenKey, string(data)); err != nil { + // Fallback: write to file with strict 0600 permissions + return saveTokenToEncryptedFile(data) + } + // If keyring succeeded, remove any legacy plaintext file + removeLegacyTokenFile() + return nil +} + +// GetTokenSecurely retrieves the auth token from the OS keyring. +// Falls back to the encrypted file if keyring is unavailable. +func GetTokenSecurely() (*StoredToken, error) { + secret, err := keyring.Get(ServiceName, TokenKey) + if err != nil { + // Fallback: try encrypted file + return getTokenFromEncryptedFile() + } + var token StoredToken + if err := json.Unmarshal([]byte(secret), &token); err != nil { + return nil, fmt.Errorf("failed to deserialize token from keyring: %w", err) + } + return &token, nil +} + +// DeleteTokenSecurely removes the stored token from the keyring. +func DeleteTokenSecurely() error { + _ = keyring.Delete(ServiceName, TokenKey) + removeLegacyTokenFile() + return nil +} + +// legacyTokenPath returns the path to the old plaintext token file. +func legacyTokenPath() string { + configDir := os.Getenv("XDG_CONFIG_HOME") + if configDir == "" { + home, _ := os.UserHomeDir() + configDir = filepath.Join(home, ".config") + } + return filepath.Join(configDir, "ghaymah", "cli", "nhost", "config.json") +} + +// removeLegacyTokenFile securely deletes the old plaintext config.json. +func removeLegacyTokenFile() { + path := legacyTokenPath() + if _, err := os.Stat(path); err == nil { + // Overwrite with zeros before deleting to prevent recovery + if f, err := os.OpenFile(path, os.O_WRONLY, 0600); err == nil { + info, _ := f.Stat() + zeros := make([]byte, info.Size()) + f.Write(zeros) + f.Close() + } + os.Remove(path) + } +} + +// saveTokenToEncryptedFile is the fallback for headless/CI environments. +func saveTokenToEncryptedFile(data []byte) error { + path := legacyTokenPath() + dir := filepath.Dir(path) + if err := os.MkdirAll(dir, 0700); err != nil { + return err + } + // NOTE: In production, encrypt `data` with a machine-specific key + // (e.g., derived from machine-id + HKDF) before writing. + return os.WriteFile(path, data, 0600) +} + +func getTokenFromEncryptedFile() (*StoredToken, error) { + data, err := os.ReadFile(legacyTokenPath()) + if err != nil { + return nil, err + } + var token StoredToken + if err := json.Unmarshal(data, &token); err != nil { + return nil, err + } + return &token, nil +} + +func init() { + // Print a warning if we detect a legacy plaintext token file + if _, err := os.Stat(legacyTokenPath()); err == nil { + fmt.Fprintf(os.Stderr, + "⚠️ Security: Legacy plaintext token file detected at %s\n"+ + " Run 'gy login' to migrate to secure OS keyring storage.\n", + legacyTokenPath(), + ) + } + _ = runtime.GOOS // suppress unused import +} +``` + +--- + +### PATCH-03: Debug Log Redaction Filter (Fixes NEW-VULN-04) + +**Target:** `pkg/logger/redactor.go` *(new file)* + +```go +// Package logger provides a redaction filter that masks sensitive data +// (UUIDs, Bearer tokens, emails, API keys) in debug output. +package logger + +import ( + "io" + "regexp" +) + +// Patterns to redact from log output +var redactionPatterns = []*regexp.Regexp{ + // UUIDs (e.g., user_id=6c8a1ce5-4ac3-47ad-8838-64e2a986acf3) + regexp.MustCompile(`([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})`), + // Bearer tokens + regexp.MustCompile(`(Bearer\s+)[A-Za-z0-9\-._~+/]+=*`), + // JWT tokens (eyJ...) + regexp.MustCompile(`(eyJ[A-Za-z0-9\-_]+\.eyJ[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+)`), + // Email addresses + regexp.MustCompile(`([a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,})`), + // API keys (long alphanumeric strings after common key patterns) + regexp.MustCompile(`((?:api[_-]?key|token|secret|password)\s*[=:]\s*)[^\s"',]+`), +} + +var redactionReplacements = []string{ + "${1}****-****-****-****", // UUID β†’ partially masked + "${1}[REDACTED]", // Bearer + "[JWT-REDACTED]", // JWT + "[EMAIL-REDACTED]", // Email + "${1}[REDACTED]", // API key +} + +// RedactSensitiveData applies all redaction patterns to the input string. +func RedactSensitiveData(input string) string { + result := input + for i, pattern := range redactionPatterns { + result = pattern.ReplaceAllString(result, redactionReplacements[i]) + } + return result +} + +// RedactingWriter wraps an io.Writer and redacts sensitive data before writing. +type RedactingWriter struct { + Underlying io.Writer +} + +func (rw *RedactingWriter) Write(p []byte) (n int, err error) { + redacted := RedactSensitiveData(string(p)) + return rw.Underlying.Write([]byte(redacted)) +} + +// NewRedactingWriter creates a writer that automatically masks sensitive data. +// Usage: pass this as the output writer for the debug logger. +// +// logger.SetOutput(NewRedactingWriter(os.Stderr)) +func NewRedactingWriter(w io.Writer) *RedactingWriter { + return &RedactingWriter{Underlying: w} +} +``` + +**Integration in the logger initialization:** +```go +// In your logger setup (e.g., cmd/root.go or wherever slog/logrus is configured): +if debugMode { + log.SetOutput(logger.NewRedactingWriter(os.Stderr)) +} +``` + +--- + +### PATCH-04: Secure Auth Client with Proxy Bypass & TLS Pinning (Fixes NEW-VULN-05) + +**Target:** `pkg/auth/secureclient.go` *(new file)* + +```go +// Package auth provides a hardened HTTP client for authentication endpoints +// that bypasses system proxies and enforces TLS certificate pinning. +package auth + +import ( + "crypto/sha256" + "crypto/tls" + "crypto/x509" + "encoding/hex" + "fmt" + "net" + "net/http" + "os" + "strings" + "time" +) + +// PinnedCertHashes contains SHA-256 hashes of the SubjectPublicKeyInfo (SPKI) +// for all trusted *.ghaymah.systems certificates. +// Generate with: openssl x509 -in cert.pem -pubkey -noout | openssl pkey -pubin -outform DER | sha256sum +var PinnedCertHashes = []string{ + // TODO: Replace with the actual SPKI hash of *.ghaymah.systems + // "ab:cd:ef:01:23:45:67:89:ab:cd:ef:01:23:45:67:89:ab:cd:ef:01:23:45:67:89:ab:cd:ef:01:23:45:67:89", +} + +// AuthEndpoints lists the hostnames that must use the secure, proxy-free client. +var AuthEndpoints = []string{ + "auth.ghaymah.systems", + "graphql.ghaymah.systems", +} + +// isAuthEndpoint checks if the given host is a sensitive authentication endpoint. +func isAuthEndpoint(host string) bool { + // Strip port if present + h, _, err := net.SplitHostPort(host) + if err != nil { + h = host + } + for _, ep := range AuthEndpoints { + if strings.EqualFold(h, ep) { + return true + } + } + return false +} + +// NewSecureAuthClient creates an HTTP client that: +// 1. Ignores HTTP_PROXY/HTTPS_PROXY for authentication endpoints +// 2. Enforces TLS 1.3 minimum +// 3. Validates server certificate SPKI hash against pinned values +// 4. Warns the user if a system proxy was detected and bypassed +func NewSecureAuthClient() *http.Client { + // Detect and warn about proxy environment + if proxy := os.Getenv("HTTPS_PROXY"); proxy != "" { + fmt.Fprintf(os.Stderr, + "⚠️ Security Warning: HTTPS_PROXY=%s detected.\n"+ + " Authentication requests will bypass the proxy for security.\n"+ + " Use --allow-proxy-auth to override (NOT recommended).\n", + proxy, + ) + } + + transport := &http.Transport{ + // 1. Explicitly disable proxy for auth endpoints + Proxy: nil, + TLSClientConfig: &tls.Config{ + MinVersion: tls.VersionTLS13, + // 2. Custom certificate verification with pinning + VerifyPeerCertificate: func(rawCerts [][]byte, chains [][]*x509.Certificate) error { + if len(PinnedCertHashes) == 0 { + // Pinning not configured yet β€” allow standard verification + return nil + } + for _, rawCert := range rawCerts { + cert, err := x509.ParseCertificate(rawCert) + if err != nil { + continue + } + // Hash the SubjectPublicKeyInfo + spkiHash := sha256.Sum256(cert.RawSubjectPublicKeyInfo) + hexHash := hex.EncodeToString(spkiHash[:]) + + for _, pinned := range PinnedCertHashes { + cleaned := strings.ReplaceAll(pinned, ":", "") + if strings.EqualFold(hexHash, cleaned) { + return nil // Pin matched + } + } + } + return fmt.Errorf( + "TLS certificate pinning failure: server certificate does not match " + + "any pinned SPKI hash for *.ghaymah.systems β€” possible MITM attack", + ) + }, + }, + // 3. Sensible timeouts + TLSHandshakeTimeout: 10 * time.Second, + ResponseHeaderTimeout: 15 * time.Second, + } + + return &http.Client{ + Transport: transport, + Timeout: 30 * time.Second, + } +} +``` + +--- + +### PATCH-05: Secure Auto-Update with Checksum Verification (Fixes NEW-VULN-06) + +**Target:** `pkg/update/verifier.go` *(new file)* + +```go +// Package update provides cryptographic integrity verification for CLI auto-updates. +package update + +import ( + "crypto/sha256" + "encoding/hex" + "fmt" + "io" + "net/http" + "os" + "strings" + "time" +) + +// VersionManifest represents the expected response from https://cli.ghaymah.systems/version +type VersionManifest struct { + Version string `json:"version"` + BinarySHA string `json:"sha256"` // SHA-256 hex hash of the binary + InstallerSHA string `json:"installer_sha"` // SHA-256 hex hash of install.sh + MinVersion string `json:"min_version"` // Minimum supported version +} + +// VerifyFileChecksum computes the SHA-256 hash of a file and compares it +// against the expected hex-encoded hash from the version manifest. +func VerifyFileChecksum(filePath, expectedSHA256Hex string) error { + f, err := os.Open(filePath) + if err != nil { + return fmt.Errorf("cannot open file for verification: %w", err) + } + defer f.Close() + + hasher := sha256.New() + if _, err := io.Copy(hasher, f); err != nil { + return fmt.Errorf("failed to read file for hashing: %w", err) + } + + computedHash := hex.EncodeToString(hasher.Sum(nil)) + expectedClean := strings.TrimSpace(strings.ToLower(expectedSHA256Hex)) + computedClean := strings.ToLower(computedHash) + + if computedClean != expectedClean { + return fmt.Errorf( + "INTEGRITY CHECK FAILED β€” possible supply-chain attack!\n"+ + " Expected SHA-256: %s\n"+ + " Computed SHA-256: %s\n"+ + " File: %s\n"+ + " The downloaded update has been REJECTED and deleted.", + expectedClean, computedClean, filePath, + ) + } + return nil +} + +// SecureDownload downloads a URL to a temporary file, verifies its checksum, +// and only then moves it to the destination path. +func SecureDownload(url, destPath, expectedSHA256 string) error { + // 1. Download to a temporary file + tmpFile, err := os.CreateTemp("", "gy-update-*") + if err != nil { + return fmt.Errorf("failed to create temp file: %w", err) + } + tmpPath := tmpFile.Name() + defer os.Remove(tmpPath) // Clean up on any error path + + client := &http.Client{Timeout: 120 * time.Second} + resp, err := client.Get(url) + if err != nil { + tmpFile.Close() + return fmt.Errorf("download failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + tmpFile.Close() + return fmt.Errorf("download failed: HTTP %d from %s", resp.StatusCode, url) + } + + if _, err := io.Copy(tmpFile, resp.Body); err != nil { + tmpFile.Close() + return fmt.Errorf("download incomplete: %w", err) + } + tmpFile.Close() + + // 2. Verify checksum BEFORE moving to destination + if err := VerifyFileChecksum(tmpPath, expectedSHA256); err != nil { + return err + } + + // 3. Checksum passed β€” atomically move to destination + if err := os.Rename(tmpPath, destPath); err != nil { + // Cross-device rename fallback: copy + delete + return copyFile(tmpPath, destPath) + } + return os.Chmod(destPath, 0755) +} + +func copyFile(src, dst string) error { + in, err := os.Open(src) + if err != nil { + return err + } + defer in.Close() + out, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0755) + if err != nil { + return err + } + defer out.Close() + _, err = io.Copy(out, in) + return err +} +``` + +--- + +### PATCH-06: GenAI API Key Secure Input (Fixes NEW-VULN-07) + +**Target:** `pkg/llm/securekey.go` *(new file)* + +```go +// Package llm provides secure API key handling for GenAI integration. +package llm + +import ( + "bufio" + "fmt" + "os" + "strings" + + "golang.org/x/term" +) + +// ReadAPIKeySecurely reads the GenAI API key through a priority chain: +// 1. --ai-key-stdin flag β†’ read from stdin (no echo, safe for piping) +// 2. OS keyring (if previously saved) +// 3. Interactive terminal prompt (masked input) +// +// This avoids exposing the key via /proc/PID/cmdline or /proc/PID/environ. +func ReadAPIKeySecurely(flagValue string) (string, error) { + // Priority 1: If explicitly passed via flag (backward compat, but warn) + if flagValue != "" { + fmt.Fprintf(os.Stderr, + "⚠️ Warning: Passing API keys via command-line flags exposes them in "+ + "process listings (/proc/PID/cmdline).\n"+ + " Consider using --ai-key-stdin or 'gy config set ai-key' instead.\n", + ) + return flagValue, nil + } + + // Priority 2: Environment variable (backward compat, but warn) + if envKey := os.Getenv("GY_AI_KEY"); envKey != "" { + fmt.Fprintf(os.Stderr, + "⚠️ Warning: GY_AI_KEY environment variable detected. This is readable "+ + "via /proc/PID/environ on Linux.\n"+ + " Consider using --ai-key-stdin or 'gy config set ai-key' instead.\n", + ) + return envKey, nil + } + + // Priority 3: Read from stdin if data is being piped + stat, _ := os.Stdin.Stat() + if (stat.Mode() & os.ModeCharDevice) == 0 { + // Data is being piped via stdin + scanner := bufio.NewScanner(os.Stdin) + if scanner.Scan() { + return strings.TrimSpace(scanner.Text()), nil + } + return "", fmt.Errorf("no API key provided via stdin") + } + + // Priority 4: Interactive secure prompt (masked) + fmt.Fprint(os.Stderr, "πŸ”‘ Enter GenAI API key: ") + keyBytes, err := term.ReadPassword(int(os.Stdin.Fd())) + fmt.Fprintln(os.Stderr) // newline after masked input + if err != nil { + return "", fmt.Errorf("failed to read API key: %w", err) + } + + key := strings.TrimSpace(string(keyBytes)) + if key == "" { + return "", fmt.Errorf("API key cannot be empty") + } + return key, nil +} +``` + +--- + +### PATCH-07: Symlink-Safe File Reader (Fixes NEW-VULN-08) + +**Target:** `pkg/buildpack/safefile.go` *(new file)* + +```go +// Package buildpack provides safe file I/O that prevents symlink traversal attacks +// during deployment directory packaging. +package buildpack + +import ( + "fmt" + "io/fs" + "os" + "path/filepath" + "strings" +) + +// ReadFileSecurely reads a file from within baseDir, rejecting any symbolic links +// and ensuring the resolved path stays strictly inside the deployment root. +// +// This replaces direct os.ReadFile / os.Open calls in the deployment packager. +func ReadFileSecurely(baseDir, relativePath string) ([]byte, error) { + // 1. Clean and resolve the base directory + absBase, err := filepath.Abs(baseDir) + if err != nil { + return nil, fmt.Errorf("failed to resolve base directory: %w", err) + } + + // 2. Construct the target path + targetPath := filepath.Join(absBase, filepath.Clean(relativePath)) + + // 3. Use Lstat (NOT Stat) to inspect WITHOUT following symlinks + info, err := os.Lstat(targetPath) + if err != nil { + return nil, fmt.Errorf("cannot access %s: %w", relativePath, err) + } + + // 4. Reject symbolic links + if info.Mode()&os.ModeSymlink != 0 { + linkTarget, _ := os.Readlink(targetPath) + return nil, fmt.Errorf( + "πŸ›‘ SECURITY: Symbolic link detected and BLOCKED\n"+ + " File: %s\n"+ + " Target: %s\n"+ + " Symlinks in deployment directories are not allowed to prevent "+ + "local file exfiltration (CWE-59).\n"+ + " Replace the symlink with the actual file content.", + relativePath, linkTarget, + ) + } + + // 5. Resolve the real path and verify it's inside the deployment root + realPath, err := filepath.EvalSymlinks(targetPath) + if err != nil { + return nil, fmt.Errorf("failed to resolve real path for %s: %w", relativePath, err) + } + absReal, _ := filepath.Abs(realPath) + if !strings.HasPrefix(absReal, absBase+string(filepath.Separator)) && absReal != absBase { + return nil, fmt.Errorf( + "πŸ›‘ SECURITY: Path traversal detected and BLOCKED\n"+ + " File: %s\n"+ + " Resolved: %s\n"+ + " Deploy Root: %s\n"+ + " The resolved file path is outside the deployment directory.", + relativePath, absReal, absBase, + ) + } + + // 6. Safe to read + return os.ReadFile(realPath) +} + +// WalkDirSecurely walks a deployment directory, skipping all symlinks. +// Use this instead of filepath.WalkDir in the deployment packager. +func WalkDirSecurely(baseDir string, fn func(path string, d fs.DirEntry) error) error { + absBase, err := filepath.Abs(baseDir) + if err != nil { + return err + } + + return filepath.WalkDir(absBase, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + + // Check for symlinks via Lstat + info, lErr := os.Lstat(path) + if lErr != nil { + return lErr + } + if info.Mode()&os.ModeSymlink != 0 { + linkTarget, _ := os.Readlink(path) + fmt.Fprintf(os.Stderr, + "⚠️ Skipping symlink: %s β†’ %s (security policy)\n", + path, linkTarget, + ) + if d.IsDir() { + return filepath.SkipDir + } + return nil + } + + // Verify path stays inside base + absPath, _ := filepath.Abs(path) + if !strings.HasPrefix(absPath, absBase) { + return fmt.Errorf("path escape detected: %s is outside %s", absPath, absBase) + } + + return fn(path, d) + }) +} +``` + +--- + +## πŸ“… Remediation Roadmap & Timeline + +```mermaid +gantt + title Ghaymah CLI v2 β€” Prioritized Remediation Roadmap + dateFormat YYYY-MM-DD + axisFormat %b %d + + section πŸ”΄ Critical (Week 1–2) + PATCH-04 TLS Pinning + Proxy Bypass (VULN-05) :crit, active, p4, 2026-09-01, 5d + PATCH-07 Symlink Traversal Prevention (VULN-08) :crit, active, p7, 2026-09-01, 3d + Dockerfile .dockerignore Scaffolding (VULN-03) :crit, p3, after p7, 2d + OTP + Password Change Verification (VULN-04) :crit, p4b, after p4, 4d + + section 🟠 High (Week 2–3) + PATCH-02 OS Keyring Token Storage (VULN-03) :p2, 2026-09-08, 5d + PATCH-05 Signed Auto-Update Verification (VULN-06) :p5, 2026-09-08, 5d + PATCH-03 Debug Log Redaction Filter (VULN-04) :p3b, after p2, 3d + Token Revocation Deny-List Backend (VULN-08) :p8, after p5, 4d + + section 🟑 Medium (Week 3–4) + PATCH-01 Port + Name Validation (VULN-01/02) :p1, 2026-09-15, 3d + PATCH-06 Secure AI Key Input (VULN-07) :p6, after p1, 2d + Deploy Timeout + Cancel Command (VULN-12) :p12, after p6, 3d + Integration Testing + Regression Suite :milestone, 2026-09-22, 0d +``` + +### Priority Justification + +| Priority | Patches | Rationale | +| :---: | :--- | :--- | +| **P0 β€” Immediate** | PATCH-04 (Proxy), PATCH-07 (Symlink) | Active exploitation paths: credential theft and arbitrary file exfiltration require zero user interaction beyond a poisoned environment variable or a malicious symlink. | +| **P1 β€” This Sprint** | PATCH-02 (Keyring), PATCH-05 (Updates), PATCH-03 (Redactor) | Post-authentication data protection: stolen tokens enable persistent access; unsigned updates enable RCE. | +| **P2 β€” Next Sprint** | PATCH-01 (Validation), PATCH-06 (AI Key) | Defense-in-depth: the backend may partially validate these inputs, but client-side guards reduce attack surface and improve UX. | diff --git a/Reports/Ghaymah_CLI_v2_Reverse_Engineering_Report.md b/Reports/Ghaymah_CLI_v2_Reverse_Engineering_Report.md new file mode 100644 index 0000000..f10246b --- /dev/null +++ b/Reports/Ghaymah_CLI_v2_Reverse_Engineering_Report.md @@ -0,0 +1,519 @@ +# πŸ”¬ Complete Reverse Engineering Report β€” Ghaymah CLI v2 Binary +## Full CLI Map + `deploy` Endpoint Deep-Dive + +**Binary:** `gy-linux-amd64` | **Format:** ELF 64-bit x86-64 | **Size:** 8.7 MB (9,154,722 bytes) +**Go Version:** `go1.26.6` | **CLI Version:** `0.0.24` | **Module:** `gitlab.com/ghaymahdevqateam/ghaymahcli/ghaymah-cli` + +--- + +## πŸ“¦ 1. Complete Go Dependency Tree (go.mod Reverse-Engineered) + +| Dependency | Version | Purpose | +|:---|:---|:---| +| `github.com/spf13/cobra` | `v1.9.1` | CLI framework (commands, flags, aliases) | +| `github.com/spf13/pflag` | `v1.0.6` | POSIX flag parsing | +| `gitlab.com/ghaymah/go-utils` | `v0.0.0-20250705142236` | Nhost auth client + GraphQL client | +| `gitlab.com/ghaymahdevqateam/ghaymahcli/ghaymah-tunnel` | `v0.0.0` | Tunnel functionality (local replacement) | +| `github.com/flexstack/new-dockerfile` | `v0.0.0` | Auto-Dockerfile generation engine | +| `github.com/gorilla/websocket` | `v1.5.3` | WebSocket for GraphQL subscriptions | +| `github.com/google/uuid` | `v1.6.0` | UUID generation for resource IDs | +| `github.com/fatih/color` | `v1.18.0` | Terminal color output | +| `github.com/Masterminds/semver/v3` | `v3.4.0` | Semantic versioning for auto-update | +| `github.com/go-playground/validator/v10` | `v10.23.0` | Struct validation | +| `github.com/tiktoken-go/tokenizer` | `v0.6.2` | LLM tokenization for GenAI | +| `github.com/kaptinlin/jsonrepair` | `v0.1.1` | JSON repair for LLM responses | +| `github.com/dlclark/regexp2` | `v1.11.5` | Advanced regex for detection | +| `github.com/go-git/go-git/v5` | `v5.13.1` | Git operations (.gitignore parsing) | +| `github.com/gabriel-vasile/mimetype` | `v1.4.8` | File MIME type detection | +| `github.com/pelletier/go-toml/v2` | `v2.2.3` | TOML config parsing | +| `github.com/cyphar/filepath-securejoin` | `v0.4.1` | Secure filepath joining | +| `golang.org/x/crypto` | `v0.31.0` | TLS/crypto primitives | +| `golang.org/x/term` | `v0.27.0` | Terminal password input | + +--- + +## πŸ—ΊοΈ 2. Complete CLI Command Map + +### Internal Package Architecture (Reverse-Engineered from Symbols) + +``` +gitlab.com/ghaymahdevqateam/ghaymahcli/ghaymah-cli/ +β”œβ”€β”€ cmd/ # Cobra command handlers +β”‚ β”œβ”€β”€ root.go # Root command + global flags (--debug, --json, --file, --no-auto-update) +β”‚ β”œβ”€β”€ deploy.go # Deploy command handler +β”‚ β”œβ”€β”€ config.go # Config show/set/unset/sync +β”‚ β”œβ”€β”€ login.go # Login command +β”‚ β”œβ”€β”€ signup.go # Signup command +β”‚ β”œβ”€β”€ logout.go # Logout command +β”‚ β”œβ”€β”€ whoami.go # Whoami command +β”‚ β”œβ”€β”€ list.go # List apps/projects/integrations +β”‚ β”œβ”€β”€ info.go # Info/inspect command +β”‚ β”œβ”€β”€ delete.go # Delete app/project/integration +β”‚ β”œβ”€β”€ logs.go # Log streaming command +β”‚ β”œβ”€β”€ tunnel.go # Tunnel start/list +β”‚ β”œβ”€β”€ version.go # Version command +β”‚ └── completion.go # Shell completions (bash, zsh, fish, powershell) +β”‚ +β”œβ”€β”€ pkg/ +β”‚ β”œβ”€β”€ buildpack/ # 🎯 Dockerfile generation + artifact packaging +β”‚ β”‚ β”œβ”€β”€ detect.go # Project type detection engine +β”‚ β”‚ β”œβ”€β”€ dockerfile.go # Dockerfile template generation +β”‚ β”‚ β”œβ”€β”€ readFileMax() # File reader (SYMLINK VULNERABLE) +β”‚ β”‚ └── artifact.go # tar.gz artifact creation + S3 upload +β”‚ β”‚ +β”‚ β”œβ”€β”€ resources/ # GraphQL resource management +β”‚ β”‚ β”œβ”€β”€ ResourceManager # Core CRUD manager +β”‚ β”‚ β”œβ”€β”€ AppResource # App resource type +β”‚ β”‚ β”œβ”€β”€ ProjectResource # Project resource type +β”‚ β”‚ β”œβ”€β”€ IntegrationResource # Integration resource type +β”‚ β”‚ β”œβ”€β”€ ContainerConfig # Container config struct +β”‚ β”‚ β”œβ”€β”€ PortConfig # Port configuration +β”‚ β”‚ β”œβ”€β”€ PublicAccessConfig # Public access toggle +β”‚ β”‚ β”œβ”€β”€ SSHConfig # SSH configuration +β”‚ β”‚ β”œβ”€β”€ SourceConfig # Source code config +β”‚ β”‚ β”œβ”€β”€ ChartInfo # Helm chart info +β”‚ β”‚ └── startSubscription() # WebSocket GraphQL subscription +β”‚ β”‚ +β”‚ β”œβ”€β”€ llm/ # GenAI / LLM integration +β”‚ β”‚ β”œβ”€β”€ DockerfileHint # LLM-generated Dockerfile hints +β”‚ β”‚ └── chatMessage # LLM chat message struct +β”‚ β”‚ +β”‚ β”œβ”€β”€ wizard/ # Interactive deployment wizard +β”‚ β”‚ β”œβ”€β”€ Run() # Wizard entry point +β”‚ β”‚ β”œβ”€β”€ askChoice() # Interactive choice selector +β”‚ β”‚ β”œβ”€β”€ askString() # String input prompt +β”‚ β”‚ β”œβ”€β”€ askInt() # Integer input prompt +β”‚ β”‚ β”œβ”€β”€ askConfirm() # Y/N confirmation +β”‚ β”‚ β”œβ”€β”€ askEnvVars() # Environment variable input +β”‚ β”‚ └── manualConfig() # Manual config fallback +β”‚ β”‚ +β”‚ └── config/ # Local config management (.gy.json) +β”‚ └── ConfigManager # Config file read/write +β”‚ +└── main.go # Entry point +``` + +### Full Command Tree with Aliases and Flags + +``` +gy (root) +β”œβ”€β”€ Global Flags: +β”‚ β”œβ”€β”€ --debug Enable debug output (slog DEBUG level) +β”‚ β”œβ”€β”€ -f, --file string Input file (- for stdin) +β”‚ β”œβ”€β”€ --json Output in JSON format +β”‚ └── --no-auto-update Skip automatic CLI updates +β”‚ +β”œβ”€β”€ deploy [PATH] aliases: d, push, up +β”‚ β”œβ”€β”€ -n, --name string App name (defaults to directory name) +β”‚ β”œβ”€β”€ -p, --project string Project name (auto-creates if new) +β”‚ β”œβ”€β”€ --port int Port (auto-detected from project type) +β”‚ β”œβ”€β”€ --tier string Resource tier: t1, t2, t3 (default: t1) +β”‚ β”œβ”€β”€ --public Make publicly accessible (default: true) +β”‚ β”œβ”€β”€ --domain string Custom domain +β”‚ β”œβ”€β”€ --dockerfile string Path to custom Dockerfile +β”‚ β”œβ”€β”€ --ai-key string GenAI API key (or GY_AI_KEY env) +β”‚ β”œβ”€β”€ --no-ai Disable AI-assisted detection +β”‚ └── -w, --wizard Interactive guided setup +β”‚ +β”œβ”€β”€ config aliases: cfg, edit, settings +β”‚ β”œβ”€β”€ config [show] Show current .gy.json config +β”‚ β”œβ”€β”€ config set Set values (tier, public, domain, port, env KEY=VALUE) +β”‚ β”œβ”€β”€ config unset env KEY Remove env var +β”‚ └── config sync Push .gy.json to server (without redeploy) +β”‚ +β”œβ”€β”€ login aliases: l +β”‚ β”œβ”€β”€ -e, --email string +β”‚ └── -p, --password string +β”‚ +β”œβ”€β”€ signup aliases: register +β”‚ β”œβ”€β”€ -e, --email string +β”‚ └── -p, --password string +β”‚ +β”œβ”€β”€ logout aliases: lo +β”œβ”€β”€ whoami aliases: status, me +β”‚ +β”œβ”€β”€ list [type] aliases: ls +β”‚ └── types: apps, projects, integrations +β”‚ +β”œβ”€β”€ info [NAME] aliases: inspect, show, get +β”‚ β”œβ”€β”€ --id string Look up by UUID +β”‚ └── --project Look up a project +β”‚ +β”œβ”€β”€ delete aliases: rm, remove, del +β”‚ β”œβ”€β”€ types: app, project, integration +β”‚ └── --force Skip confirmation +β”‚ +β”œβ”€β”€ logs [NAME] aliases: log, o +β”‚ β”œβ”€β”€ -n, --tail int Lines from end (default: 100) +β”‚ β”œβ”€β”€ --follow Stream continuously (default: true) +β”‚ β”œβ”€β”€ --no-follow Print and exit +β”‚ └── --previous Previous container run logs +β”‚ +β”œβ”€β”€ tunnel aliases: t, tun +β”‚ β”œβ”€β”€ tunnel start NAME aliases: s, up +β”‚ β”‚ └── -p, --port int Local port (default: 8080) +β”‚ └── tunnel list aliases: ls +β”‚ +β”œβ”€β”€ version Show CLI version +└── completion Generate shell completions (bash/zsh/fish/powershell) +``` + +--- + +## 🎯 3. `deploy` Endpoint β€” Complete Deep-Dive + +### 3.1 Deploy Execution Flow (Step-by-Step) + +The deploy command orchestrates a **10-step pipeline**. Here is the exact sequence reconstructed from binary symbols, debug output, and string analysis: + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ gy deploy [PATH] β€” FULL PIPELINE β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + + Step 1: AUTO-UPDATE CHECK + β”‚ β†’ GET https://cli.ghaymah.systems/version + β”‚ β†’ Compare semver (Masterminds/semver) + β”‚ β†’ If newer: download https://cli.ghaymah.systems/install.sh + β”‚ β†’ Execute & restart (NO signature check ⚠️ VULN-06) + β”‚ β†’ Skip if --no-auto-update + β–Ό + Step 2: AUTHENTICATION + β”‚ β†’ Load token from ~/.config/ghaymah/cli/nhost/config.json + β”‚ β†’ nhost.tokenManager.getToken() + β”‚ β†’ If expired: POST https://auth.ghaymah.systems/token (refresh) + β”‚ β†’ If no token: error "run 'gy login' first" + β”‚ β†’ Respects HTTP_PROXY/HTTPS_PROXY (⚠️ VULN-05) + β–Ό + Step 3: RESOLVE DEPLOY PATH + β”‚ β†’ Default: current working directory + β”‚ β†’ Or: explicit PATH argument + β”‚ β†’ Read .gy.json if present (name, port, project, env vars) + β”‚ β†’ Derive app name from directory basename if --name not set + β–Ό + Step 4: PROJECT TYPE DETECTION (pkg/buildpack/detect.go) + β”‚ β†’ Scan files in deploy directory: + β”‚ β”‚ β”œβ”€β”€ go.mod, main.go β†’ Go + β”‚ β”‚ β”œβ”€β”€ package.json β†’ Node.js (then check framework) + β”‚ β”‚ β”‚ β”œβ”€β”€ next.config.* β†’ Next.js (Standalone) + β”‚ β”‚ β”‚ β”œβ”€β”€ "express" in deps β†’ Express + β”‚ β”‚ β”‚ β”œβ”€β”€ "fastify" in deps β†’ Fastify + β”‚ β”‚ β”‚ └── (default) β†’ Node.js generic + β”‚ β”‚ β”œβ”€β”€ requirements.txt/Pipfile/pyproject.toml/uv.lock β†’ Python + β”‚ β”‚ β”‚ β”œβ”€β”€ manage.py β†’ Django + β”‚ β”‚ β”‚ β”œβ”€β”€ fastapi β†’ FastAPI + β”‚ β”‚ β”‚ └── (default) β†’ Python generic + β”‚ β”‚ β”œβ”€β”€ Gemfile β†’ Ruby (Rails/Sinatra) + β”‚ β”‚ β”œβ”€β”€ mix.exs β†’ Elixir (Phoenix) + β”‚ β”‚ β”œβ”€β”€ Cargo.toml β†’ Rust + β”‚ β”‚ β”œβ”€β”€ pom.xml/build.gradle β†’ Java (Maven/Gradle) + β”‚ β”‚ β”œβ”€β”€ composer.json β†’ PHP (Laravel/Symfony) + β”‚ β”‚ β”œβ”€β”€ deno.json/deno.lock β†’ Deno + β”‚ β”‚ β”œβ”€β”€ bun.lockb β†’ Bun + β”‚ β”‚ β”œβ”€β”€ index.html / nginx.conf β†’ Static Site + β”‚ β”‚ └── (none matched) β†’ Unknown + β”‚ β”‚ + β”‚ β†’ If --wizard: interactive wizard.Run() for manual config + β”‚ β†’ If --ai-key or GY_AI_KEY: call GenAI for detection assistance + β”‚ β†’ Output: "Detected: {framework}" or "Unknown" + β–Ό + Step 5: DOCKERFILE RESOLUTION + β”‚ β†’ Check 1: --dockerfile flag β†’ use custom Dockerfile path + β”‚ β†’ Check 2: Existing Dockerfile in deploy dir β†’ "Detected: Existing Dockerfile" + β”‚ β†’ Check 3: Auto-generate from detected project type + β”‚ β”‚ β”œβ”€β”€ Uses github.com/flexstack/new-dockerfile library + β”‚ β”‚ β”œβ”€β”€ Selects template based on detection (13 templates): + β”‚ β”‚ β”‚ Node.js, Next.js, Python, Go, Ruby, Rust, Java, + β”‚ β”‚ β”‚ PHP, Elixir, Deno, Bun, Static Site, Generic + β”‚ β”‚ β”œβ”€β”€ Templates use multi-stage builds: + β”‚ β”‚ β”‚ FROM {base}:{version} AS builder + β”‚ β”‚ β”‚ ... install deps, build ... + β”‚ β”‚ β”‚ FROM {runtime}:{version} + β”‚ β”‚ β”‚ COPY --from=builder ... + β”‚ β”‚ β”‚ EXPOSE {port} + β”‚ β”‚ β”‚ CMD [...] + β”‚ β”‚ β”œβ”€β”€ Applies nonroot user (addgroup/adduser --system nonroot) + β”‚ β”‚ └── Writes generated Dockerfile to deploy directory + β”‚ β†’ If detection failed: "A Dockerfile was not detected ... could not auto-generate" + β–Ό + Step 6: PORT DETECTION + β”‚ β†’ Priority 1: --port flag value + β”‚ β†’ Priority 2: .gy.json port field + β”‚ β†’ Priority 3: Auto-detect from project type: + β”‚ β”‚ β”œβ”€β”€ Node.js/Next.js/Express/Fastify β†’ 3000 + β”‚ β”‚ β”œβ”€β”€ Python/Django/Flask/FastAPI β†’ 8000 + β”‚ β”‚ β”œβ”€β”€ Go β†’ 8080 + β”‚ β”‚ β”œβ”€β”€ Ruby/Rails β†’ 3000 + β”‚ β”‚ β”œβ”€β”€ PHP β†’ 80 + β”‚ β”‚ β”œβ”€β”€ Java β†’ 8080 + β”‚ β”‚ β”œβ”€β”€ Static Site β†’ 80 + β”‚ β”‚ └── Default β†’ 8080 + β”‚ β†’ Warning: "Auto-detected port: %d. If your app listens on a DIFFERENT port..." + β”‚ β†’ NO PORT VALIDATION (⚠️ VULN-01 β€” accepts -1, 99999, etc.) + β–Ό + Step 7: GRAPHQL CONNECTION + PROJECT SETUP + β”‚ β†’ Connect via WebSocket: wss://graphql.ghaymah.systems/v1/graphql + β”‚ β†’ Using github.com/gorilla/websocket + β”‚ β†’ Initialize resources.ResourceManager + β”‚ β†’ Start subscription handler (goroutine): + β”‚ β”‚ ResourceManager.startSubscription() β†’ handleSubscription() + β”‚ β”‚ β†’ processSubscriptionData() β†’ parseResource() + β”‚ β”‚ + β”‚ β†’ If project doesn't exist: + β”‚ β”‚ GraphQL Mutation: + β”‚ β”‚ mutation { + β”‚ β”‚ insert_ghaymah_cloud_resources_one(object: $object) { + β”‚ β”‚ id, name, status, ... + β”‚ β”‚ } + β”‚ β”‚ } + β”‚ β”‚ β†’ POST https://graphql.ghaymah.systems/v1/graphql + β”‚ β”‚ β†’ Waits for resource sync: WaitForResourceSync() + β”‚ β”‚ + β”‚ β†’ Reads user info from stored token (user_id UUID) + β–Ό + Step 8: ARTIFACT BUILD (pkg/buildpack) + β”‚ β†’ "Building deployment artifact..." + β”‚ β†’ Create tar.gz archive of deploy directory: + β”‚ β”‚ β”œβ”€β”€ archive/tar.NewWriter() + β”‚ β”‚ β”œβ”€β”€ compress/gzip.NewWriter() + β”‚ β”‚ β”œβ”€β”€ Walk directory files + β”‚ β”‚ β”œβ”€β”€ Read files via readFileMax() (⚠️ FOLLOWS SYMLINKS β€” VULN-08) + β”‚ β”‚ β”œβ”€β”€ Include: source code, Dockerfile, .gy.json, .env files + β”‚ β”‚ β”œβ”€β”€ Exclude: .git directory, node_modules (via .gitignore) + β”‚ β”‚ └── Bundle as deployment artifact (.tar.gz) + β”‚ β”‚ + β”‚ β†’ Upload artifact to S3-compatible storage: + β”‚ β”‚ POST https://s3-nhost-proxy-83e02743fd61.hosted.ghaymah.systems/files + β”‚ β”‚ β†’ Bucket: "ghaymah-s3-bucket" + β”‚ β”‚ β†’ Auth: Bearer token from nhost session + β”‚ β”‚ β†’ Returns: artifact file ID / URL + β–Ό + Step 9: CREATE/UPDATE APP RESOURCE + β”‚ β†’ GraphQL Mutation (insert or update): + β”‚ β”‚ insert_ghaymah_cloud_resources_one(object: { + β”‚ β”‚ name: "app-name", + β”‚ β”‚ userId: "d62f9886-...", + β”‚ β”‚ chart_name: "app", + β”‚ β”‚ values: { + β”‚ β”‚ container: { + β”‚ β”‚ image: "registry.ghaymah.systems/{user}/{app}:latest", + β”‚ β”‚ port: [{containerPort: 8080}] + β”‚ β”‚ }, + β”‚ β”‚ source: { artifactId: "...", dockerfile: "..." }, + β”‚ β”‚ publicAccess: { enabled: true/false }, + β”‚ β”‚ env: { KEY: "VALUE", ... }, + β”‚ β”‚ tier: "t1" | "t2" | "t3" + β”‚ β”‚ } + β”‚ β”‚ }) + β”‚ β”‚ + β”‚ β†’ The mutation triggers the backend build pipeline: + β”‚ β”‚ Backend pulls artifact from S3 β†’ builds Docker image β†’ + β”‚ β”‚ pushes to registry.ghaymah.systems/{user}/{app}:latest β†’ + β”‚ β”‚ deploys to Kubernetes cluster + β–Ό + Step 10: WAIT FOR DEPLOYMENT + STREAM STATUS + β”‚ β†’ ResourceManager.WaitForResourceSync() + β”‚ β†’ Subscribe to resource status updates via WebSocket + β”‚ β†’ Poll status: "pending" β†’ "building" β†’ "deploying" β†’ "running" + β”‚ β†’ Timeout: 15 minutes ("deployment timed out after 15 minutes") + β”‚ β†’ On stuck: "Your app might be stuck building. Check 'gy logs'" + β”‚ β†’ On success: display app URL "https://{name}.{domain}" + β”‚ β†’ On failure: "deployment failed: %s" + └──────────────────────────────────────────────────────────────────── + +``` + +### 3.2 Deploy Data Flow Diagram + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Local Files β”‚ β”‚ Ghaymah CLI v2 β”‚ β”‚ Ghaymah Cloud β”‚ +β”‚ β”‚ β”‚ (gy-linux-amd64) β”‚ β”‚ β”‚ +β”‚ ./ β”‚ β”‚ β”‚ β”‚ β”‚ +β”‚ β”œβ”€β”€ src/ │────▢│ 1. Detect project β”‚ β”‚ β”‚ +β”‚ β”œβ”€β”€ .gy.json β”‚ β”‚ 2. Gen Dockerfile β”‚ β”‚ β”‚ +β”‚ β”œβ”€β”€ .env β”‚ β”‚ 3. Create tar.gz β”‚ β”‚ β”‚ +β”‚ └── Dockerfileβ”‚ β”‚ 4. Upload artifact │────▢│ S3 Storage β”‚ +β”‚ β”‚ β”‚ β”‚ β”‚ (s3-nhost-proxy) β”‚ +β”‚ β”‚ β”‚ 5. GraphQL mutation │────▢│ Hasura GraphQL API β”‚ +β”‚ β”‚ β”‚ β”‚ β”‚ (graphql.ghaymah.systems)β”‚ +β”‚ β”‚ β”‚ 6. Subscribe status │◀──▢│ β”‚ +β”‚ β”‚ β”‚ (WebSocket) β”‚ β”‚ Build Pipeline β”‚ +β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ Pull artifact β”‚ +β”‚ β”‚ β”‚ 7. Stream updates │◀───│ β”œβ”€β”€ docker build β”‚ +β”‚ β”‚ β”‚ "building..." β”‚ β”‚ β”œβ”€β”€ docker push β”‚ +β”‚ β”‚ β”‚ "deploying..." β”‚ β”‚ β”‚ β†’ registry.ghaymahβ”‚ +β”‚ β”‚ β”‚ "running βœ…" β”‚ β”‚ └── k8s deploy β”‚ +β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ 8. Print app URL β”‚ β”‚ App running at: β”‚ +β”‚ β”‚ β”‚ https://{name}... β”‚ β”‚ https://{name}.hostedβ”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +### 3.3 Infrastructure Endpoints Used by Deploy + +| # | Endpoint | Protocol | Purpose in Deploy Flow | +|:-:|:---|:---|:---| +| 1 | `https://auth.ghaymah.systems/token` | HTTPS POST | Token refresh before API calls | +| 2 | `wss://graphql.ghaymah.systems/v1/graphql` | WebSocket | Real-time resource subscriptions | +| 3 | `https://graphql.ghaymah.systems/v1/graphql` | HTTPS POST | GraphQL mutations (create/update resources) | +| 4 | `https://s3-nhost-proxy-83e02743fd61.hosted.ghaymah.systems/files` | HTTPS POST | Artifact upload (tar.gz of deploy directory) | +| 5 | `registry.ghaymah.systems/{user}/{app}:latest` | Docker Registry | Backend pushes built image here | +| 6 | `https://genai.ghaymah.systems` | HTTPS POST | AI-assisted project detection (optional) | +| 7 | `https://cli.ghaymah.systems/version` | HTTPS GET | Pre-deploy auto-update check | + +### 3.4 GraphQL Mutation Format (Reconstructed) + +The core deploy mutation, reconstructed from the string `insert_ghaymah_cloud_resources_one`: + +```graphql +mutation CreateApp($object: ghaymah_cloud_resources_insert_input!) { + insert_ghaymah_cloud_resources_one(object: $object) { + id + name + status + message + chart_name + created_at + updated_at + } +} +``` + +**Variables payload:** +```json +{ + "object": { + "name": "my-app", + "userId": "d62f9886-fced-4cf2-98e4-5b62000d4f03", + "chart_name": "app", + "values": { + "container": { + "image": "registry.ghaymah.systems/d62f9886/my-app:latest", + "port": [{"containerPort": 8080}] + }, + "source": { + "artifactId": "uploaded-file-id", + "dockerfile": "Dockerfile" + }, + "publicAccess": {"enabled": true}, + "env": { + "DATABASE_URL": "postgres://...", + "NODE_ENV": "production" + }, + "tier": "t1" + } + } +} +``` + +### 3.5 Resource Status Lifecycle (WebSocket Subscription) + +``` + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ PENDING β”‚ ← Resource just created via mutation + β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜ + β”‚ + β–Ό + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚BUILDING β”‚ ← Backend pulling artifact from S3, + β”‚ β”‚ running docker build + β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜ + β”‚ + β”Œβ”€β”€β”€β”€β”΄β”€β”€β”€β”€β” + β”‚ β”‚ + β–Ό β–Ό + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚DEPLOYINGβ”‚ β”‚ FAILED β”‚ ← Build error / Dockerfile issue + β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β””β”€β”€β”€β”€β”¬β”€β”€β”€β”˜ + β”‚ + β–Ό + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚RUNNING β”‚ ← App is live at https://{name}.hosted.ghaymah.systems + β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + + Timeout: 15 minutes β†’ "deployment timed out" + Stuck: β†’ "Your app might be stuck building. Check 'gy logs'" +``` + +### 3.6 Auto-Generated Dockerfile Templates (13 Supported) + +The CLI uses `github.com/flexstack/new-dockerfile` to generate production-ready multi-stage Dockerfiles: + +| # | Project Type | Detection Files | Base Image | Template Features | +|:-:|:---|:---|:---|:---| +| 1 | **Node.js** | `package.json` | `node:{v}-alpine` | Multi-stage, frozen lockfiles, nonroot user | +| 2 | **Next.js** | `next.config.*` | `node:{v}-alpine` | Standalone output, `.next/static` copy, telemetry disabled | +| 3 | **Bun** | `bun.lockb` | `oven/bun:1` | Bun-native install, frozen lockfile | +| 4 | **Deno** | `deno.json*` | `denoland/deno:2` | `deno cache`, DENO_DIR setup | +| 5 | **Python** | `requirements.txt`, `pyproject.toml`, `uv.lock`, `Pipfile` | `python:{v}-slim` | pip/uv install, PYTHONUNBUFFERED | +| 6 | **Go** | `go.mod`, `main.go` | `golang:{v}-alpine` | CGO_ENABLED=0, static binary, scratch/alpine runtime | +| 7 | **Ruby** | `Gemfile` | `ruby:{v}-slim` | Bundle install, Rails server | +| 8 | **Rust** | `Cargo.toml` | `rust:1-slim` | Cargo build --release, debian-slim runtime | +| 9 | **Java (Maven)** | `pom.xml` | `maven:{v}-eclipse-temurin` | mvn package, JRE runtime | +| 10 | **Java (Gradle)** | `build.gradle` | `gradle:{v}-jdk` | gradle build, JRE runtime | +| 11 | **Elixir** | `mix.exs` | `elixir:{v}-slim` | mix release, OTP runtime | +| 12 | **PHP** | `composer.json` | `php:{v}-apache` | Composer install, Apache/FPM | +| 13 | **Static Site** | `index.html`, `nginx.conf` | `nginx:alpine` | Direct copy to /var/www/html | + +All templates feature: +- `nonroot` user creation (`addgroup --system nonroot && adduser --system`) +- `COPY --chown=nonroot:nonroot` for proper ownership +- Multi-stage builds to minimize final image size +- Frozen lockfile enforcement for reproducibility + +### 3.7 `.gy.json` Configuration File Format + +```json +{ + "app": "my-cool-api", + "port": 3000, + "project": "my-project", + "tier": "t1", + "publicAccess": { + "enabled": true + }, + "domain": "api.mydomain.com", + "env": { + "DATABASE_URL": "postgres://user:pass@host:5432/db", + "NODE_ENV": "production", + "DEBUG": "false" + } +} +``` + +### 3.8 Resource Tiers + +| Tier | Label | Resources | +|:---:|:---|:---| +| `t1` | Default (free/starter) | Minimal CPU/RAM | +| `t2` | Small | Increased limits | +| `t3` | Production | Full resources | + +### 3.9 Security Vulnerabilities in the Deploy Flow + +| Deploy Step | Vulnerability | Severity | Reference | +|:---|:---|:---:|:---| +| Step 1 (Auto-Update) | No signature verification on downloaded binary | 🟠 HIGH | VULN-06 | +| Step 2 (Auth) | Proxy intercepts credentials (no TLS pinning) | πŸ”΄ CRITICAL | VULN-05 | +| Step 6 (Port) | No client-side port validation | 🟑 MEDIUM | VULN-01 | +| Step 7 (GraphQL) | No client-side name sanitization | 🟑 MEDIUM | VULN-02 | +| Step 8 (Artifact) | Follows symlinks β†’ arbitrary file exfiltration | πŸ”΄ CRITICAL | VULN-08 | +| Step 8 (Artifact) | .env files included in tar.gz upload | πŸ”΄ CRITICAL | VULN-03 | +| Step 9 (App Create) | GenAI API key in env/cmdline | 🟑 MEDIUM | VULN-07 | + +--- + +## πŸ“Š 4. Complete Strings Extraction Summary + +| Category | Count | Key Findings | +|:---|:---:|:---| +| **Total extractable strings** | ~102,944 | Full string dump from ELF binary | +| **Hardcoded API endpoints** | 8 | Auth, GraphQL, S3, GenAI, CLI, Registry, Logs, Version | +| **Dockerfile templates** | 13 | Complete multi-stage Dockerfiles for all supported languages | +| **Go package symbols** | ~90 | Full function signatures for all internal packages | +| **Go dependencies (go.mod)** | 29 | All versioned deps extracted from buildinfo | +| **GraphQL operation** | 1 | `insert_ghaymah_cloud_resources_one` | +| **Error messages** | 50+ | Complete error strings for all failure paths | +| **Framework detection markers** | 30+ | File/keyword markers for all 13 project types | diff --git a/Scripts/fuzz_runner.sh b/Scripts/fuzz_runner.sh index f25ebd8..951bb7b 100644 --- a/Scripts/fuzz_runner.sh +++ b/Scripts/fuzz_runner.sh @@ -2,7 +2,7 @@ BIN="/root/ghaymah-v2-test/gy-linux-amd64" echo "=== Starting CLI Flag Fuzzing Audit ===" -COMMANDS="deploy config login list logs tunnel info delete" +COMMANDS="deploy config login list logs tunnel info delete version whoami" FLAGS="--file --json --debug --no-auto-update" TESTS=0 @@ -12,7 +12,7 @@ for cmd in $COMMANDS; do for flag in $FLAGS; do for p in ";id" "&&ls" "|cat /etc/passwd" '$(whoami)' "NULL_BYTE_\x00" "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; do TESTS=$((TESTS + 1)) - OUT=$($BIN $cmd $flag "$p" 2>&1) + OUT=$(timeout 2 $BIN $cmd $flag "$p" --no-auto-update 2>&1) if echo "$OUT" | grep -qiE "(panic|runtime error|segmentation fault|nil pointer)"; then echo "[CRITICAL PANIC DETECTED] gy $cmd $flag '$p'" echo "Output snippet: $(echo "$OUT" | head -n 3)" diff --git a/screenshots/HTTP Proxy Credential Interception_1.png b/screenshots/HTTP Proxy Credential Interception_1.png new file mode 100644 index 0000000..617f19a Binary files /dev/null and b/screenshots/HTTP Proxy Credential Interception_1.png differ diff --git a/screenshots/HTTP Proxy Credential Interception_2.png b/screenshots/HTTP Proxy Credential Interception_2.png new file mode 100644 index 0000000..911438f Binary files /dev/null and b/screenshots/HTTP Proxy Credential Interception_2.png differ diff --git a/screenshots/Screenshot_2026-08-19_163341.png b/screenshots/Screenshot_2026-08-19_163341.png new file mode 100644 index 0000000..4745c47 Binary files /dev/null and b/screenshots/Screenshot_2026-08-19_163341.png differ diff --git a/screenshots/Screenshot_2026-08-19_163501.png b/screenshots/Screenshot_2026-08-19_163501.png new file mode 100644 index 0000000..61a576b Binary files /dev/null and b/screenshots/Screenshot_2026-08-19_163501.png differ diff --git a/screenshots/docker.png b/screenshots/docker.png new file mode 100644 index 0000000..3bb9e58 Binary files /dev/null and b/screenshots/docker.png differ diff --git a/screenshots/env.png b/screenshots/env.png new file mode 100644 index 0000000..9b7e2a3 Binary files /dev/null and b/screenshots/env.png differ diff --git a/screenshots/logs.png b/screenshots/logs.png new file mode 100644 index 0000000..5576ec6 Binary files /dev/null and b/screenshots/logs.png differ