docs: add binary audit and reverse engineering reports with supporting screenshots and fuzzing scripts
@@ -316,3 +316,767 @@ user_id=d62f9886-fced-4cf2-98e4-5b62000d4f03
|
|||||||
*The output definitively proves input validation bypasses, symlink traversal, and proxy interception capabilities.*
|
*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. |
|
||||||
|
|||||||
519
Reports/Ghaymah_CLI_v2_Reverse_Engineering_Report.md
Normal file
@@ -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 <type> <NAME> 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 |
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
BIN="/root/ghaymah-v2-test/gy-linux-amd64"
|
BIN="/root/ghaymah-v2-test/gy-linux-amd64"
|
||||||
echo "=== Starting CLI Flag Fuzzing Audit ==="
|
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"
|
FLAGS="--file --json --debug --no-auto-update"
|
||||||
|
|
||||||
TESTS=0
|
TESTS=0
|
||||||
@@ -12,7 +12,7 @@ for cmd in $COMMANDS; do
|
|||||||
for flag in $FLAGS; do
|
for flag in $FLAGS; do
|
||||||
for p in ";id" "&&ls" "|cat /etc/passwd" '$(whoami)' "NULL_BYTE_\x00" "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; do
|
for p in ";id" "&&ls" "|cat /etc/passwd" '$(whoami)' "NULL_BYTE_\x00" "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; do
|
||||||
TESTS=$((TESTS + 1))
|
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
|
if echo "$OUT" | grep -qiE "(panic|runtime error|segmentation fault|nil pointer)"; then
|
||||||
echo "[CRITICAL PANIC DETECTED] gy $cmd $flag '$p'"
|
echo "[CRITICAL PANIC DETECTED] gy $cmd $flag '$p'"
|
||||||
echo "Output snippet: $(echo "$OUT" | head -n 3)"
|
echo "Output snippet: $(echo "$OUT" | head -n 3)"
|
||||||
|
|||||||
ثنائية
screenshots/HTTP Proxy Credential Interception_1.png
Normal file
|
بعد العرض: | الارتفاع: | الحجم: 218 KiB |
ثنائية
screenshots/HTTP Proxy Credential Interception_2.png
Normal file
|
بعد العرض: | الارتفاع: | الحجم: 52 KiB |
ثنائية
screenshots/Screenshot_2026-08-19_163341.png
Normal file
|
بعد العرض: | الارتفاع: | الحجم: 35 KiB |
ثنائية
screenshots/Screenshot_2026-08-19_163501.png
Normal file
|
بعد العرض: | الارتفاع: | الحجم: 134 KiB |
ثنائية
screenshots/docker.png
Normal file
|
بعد العرض: | الارتفاع: | الحجم: 30 KiB |
ثنائية
screenshots/env.png
Normal file
|
بعد العرض: | الارتفاع: | الحجم: 31 KiB |
ثنائية
screenshots/logs.png
Normal file
|
بعد العرض: | الارتفاع: | الحجم: 15 KiB |