- Increase maxAutoBuildDepth from 3 to 15 for complex dependency chains
- Remove ${ from dangerous patterns (valid shell variable expansion)
- Add support for Debian-native packages without orig.tar.gz
- Add isDebianSourceArchive() to handle native Debian source packages
720 lines
18 KiB
Go
720 lines
18 KiB
Go
package builder
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"zsvo/pkg/fetcher"
|
|
"zsvo/pkg/packager"
|
|
"zsvo/pkg/recipe"
|
|
)
|
|
|
|
// Builder handles building packages from recipes
|
|
type Builder struct {
|
|
workDir string
|
|
quiet bool
|
|
progressCallback func(BuildProgress)
|
|
envOverrides map[string]string
|
|
callbackMutex sync.RWMutex
|
|
}
|
|
|
|
// BuildProgress represents build progress state.
|
|
type BuildProgress struct {
|
|
Step int
|
|
Total int
|
|
Message string
|
|
}
|
|
|
|
// NewBuilder creates a new builder
|
|
func NewBuilder(workDir string) *Builder {
|
|
return &Builder{
|
|
workDir: workDir,
|
|
}
|
|
}
|
|
|
|
// Build builds a package from a recipe
|
|
func (b *Builder) Build(recipe *recipe.Recipe) error {
|
|
if recipe == nil {
|
|
return fmt.Errorf("recipe cannot be nil")
|
|
}
|
|
|
|
// Check if package already exists in cache
|
|
packageFile := filepath.Join(recipe.GetPackageDir(b.workDir), recipe.GetPackageFileName())
|
|
if _, err := os.Stat(packageFile); err == nil {
|
|
log.Printf("Package %s already exists, skipping build", recipe.GetPackageName())
|
|
return nil
|
|
}
|
|
|
|
totalSteps := 4 + len(recipe.Build) + len(recipe.Install)
|
|
step := 0
|
|
nextStep := func(message string) {
|
|
step++
|
|
b.reportProgress(step, totalSteps, message)
|
|
}
|
|
|
|
// Create working directories
|
|
sourceDir := recipe.GetSourceDir(b.workDir)
|
|
stagingDir := recipe.GetStagingDir(b.workDir)
|
|
packageDir := recipe.GetPackageDir(b.workDir)
|
|
|
|
nextStep("Preparing directories")
|
|
if err := b.prepareDirectories(sourceDir, stagingDir, packageDir); err != nil {
|
|
return fmt.Errorf("failed to prepare directories: %w", err)
|
|
}
|
|
|
|
// Download and extract source
|
|
nextStep("Downloading and extracting source")
|
|
if err := b.downloadAndExtract(recipe); err != nil {
|
|
return fmt.Errorf("failed to download and extract source: %w", err)
|
|
}
|
|
|
|
// Apply patches
|
|
nextStep("Applying recipe patches")
|
|
if err := b.applyPatches(recipe, sourceDir); err != nil {
|
|
return fmt.Errorf("failed to apply patches: %w", err)
|
|
}
|
|
|
|
// Validate source files
|
|
nextStep("Validating source files")
|
|
if err := b.validateSourceFiles(recipe, sourceDir); err != nil {
|
|
return fmt.Errorf("failed to validate source files: %w", err)
|
|
}
|
|
|
|
// Build package
|
|
if err := b.executeBuild(recipe, sourceDir, stagingDir, func(i, total int) {
|
|
nextStep(fmt.Sprintf("Build step %d/%d", i, total))
|
|
}); err != nil {
|
|
return fmt.Errorf("failed to build package: %w", err)
|
|
}
|
|
|
|
// Package files
|
|
if err := b.packageFiles(recipe, sourceDir, stagingDir, func(i, total int) {
|
|
nextStep(fmt.Sprintf("Install step %d/%d", i, total))
|
|
}); err != nil {
|
|
return fmt.Errorf("failed to package files: %w", err)
|
|
}
|
|
|
|
// Create package archive
|
|
nextStep("Creating package archive")
|
|
p := packager.NewPackager(b.workDir)
|
|
if err := p.Package(recipe); err != nil {
|
|
return fmt.Errorf("failed to create package archive: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// prepareDirectories creates necessary directories
|
|
func (b *Builder) prepareDirectories(sourceDir, stagingDir, packageDir string) error {
|
|
dirs := []string{
|
|
sourceDir,
|
|
stagingDir,
|
|
packageDir,
|
|
}
|
|
|
|
for _, dir := range dirs {
|
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
|
return fmt.Errorf("failed to create directory %s: %w", dir, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// downloadAndExtract downloads and extracts source
|
|
func (b *Builder) downloadAndExtract(recipe *recipe.Recipe) error {
|
|
sourceDir := recipe.GetSourceDir(b.workDir)
|
|
if err := os.RemoveAll(sourceDir); err != nil {
|
|
return fmt.Errorf("failed to clean source directory: %w", err)
|
|
}
|
|
if err := os.MkdirAll(sourceDir, 0755); err != nil {
|
|
return fmt.Errorf("failed to recreate source directory: %w", err)
|
|
}
|
|
|
|
f := fetcher.NewFetcher(filepath.Join(b.workDir, "cache"))
|
|
|
|
sourceURL := recipe.Source.URL
|
|
if recipe.Source.DebianDSC != "" {
|
|
sourceURL = recipe.Source.DebianDSC
|
|
}
|
|
|
|
return f.DownloadAndExtract(
|
|
sourceURL,
|
|
recipe.Source.Sha256,
|
|
sourceDir,
|
|
)
|
|
}
|
|
|
|
// applyPatches applies patches to source
|
|
func (b *Builder) applyPatches(recipe *recipe.Recipe, sourceDir string) error {
|
|
if len(recipe.Source.Patches) == 0 {
|
|
return nil
|
|
}
|
|
|
|
patches := make([]string, 0, len(recipe.Source.Patches))
|
|
for _, patchPath := range recipe.Source.Patches {
|
|
if patchPath == "" {
|
|
return fmt.Errorf("patch path cannot be empty")
|
|
}
|
|
|
|
if !filepath.IsAbs(patchPath) && recipe.Dir != "" {
|
|
patchPath = filepath.Join(recipe.Dir, patchPath)
|
|
}
|
|
patches = append(patches, patchPath)
|
|
}
|
|
|
|
f := fetcher.NewFetcher(filepath.Join(b.workDir, "cache"))
|
|
return f.ApplyPatches(sourceDir, patches)
|
|
}
|
|
|
|
// executeBuild executes build commands
|
|
func (b *Builder) executeBuild(recipe *recipe.Recipe, sourceDir, stagingDir string, progressFn func(step, total int)) error {
|
|
// Find the source directory (usually the first subdirectory)
|
|
srcPath, err := b.findSourceDirectory(sourceDir)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to find source directory: %w", err)
|
|
}
|
|
|
|
// Set up environment
|
|
env := b.buildEnvironment(stagingDir)
|
|
|
|
// Execute build commands
|
|
for i, cmd := range recipe.Build {
|
|
if progressFn != nil {
|
|
progressFn(i+1, len(recipe.Build))
|
|
}
|
|
|
|
// Substitute variables in command
|
|
cmd = b.substituteVariables(cmd, sourceDir, stagingDir)
|
|
|
|
if err := b.executeCommand(srcPath, cmd, env); err != nil {
|
|
return fmt.Errorf("build command %d failed: %w", i+1, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// findSourceDirectory finds the actual source directory
|
|
func (b *Builder) findSourceDirectory(sourceDir string) (string, error) {
|
|
// Validate input path
|
|
if sourceDir == "" {
|
|
return "", fmt.Errorf("source directory cannot be empty")
|
|
}
|
|
|
|
// Clean and normalize path
|
|
sourceDir = filepath.Clean(sourceDir)
|
|
|
|
entries, err := os.ReadDir(sourceDir)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to read source directory %s: %w", sourceDir, err)
|
|
}
|
|
|
|
for _, entry := range entries {
|
|
if entry.IsDir() {
|
|
return filepath.Join(sourceDir, entry.Name()), nil
|
|
}
|
|
}
|
|
|
|
return sourceDir, nil
|
|
}
|
|
|
|
// buildEnvironment builds the environment for build commands
|
|
func (b *Builder) buildEnvironment(stagingDir string) []string {
|
|
env := os.Environ()
|
|
|
|
// Add standard build environment variables
|
|
env = append(env, "DESTDIR="+stagingDir)
|
|
env = append(env, "PREFIX=/usr")
|
|
env = append(env, "PKGDIR="+stagingDir)
|
|
|
|
// Add parallel build variable
|
|
env = append(env, fmt.Sprintf("MAKEFLAGS=-j%d", runtime.NumCPU()))
|
|
|
|
return applyEnvOverrides(env, b.envOverrides)
|
|
}
|
|
|
|
// executeCommand executes a single command
|
|
func (b *Builder) executeCommand(workDir, command string, env []string) error {
|
|
if strings.TrimSpace(command) == "" {
|
|
return nil
|
|
}
|
|
|
|
// Security check
|
|
if err := b.validateCommand(command); err != nil {
|
|
return fmt.Errorf("command security validation failed: %w", err)
|
|
}
|
|
|
|
log.Printf("Executing command: %s", command)
|
|
|
|
cmd := exec.Command("sh", "-c", command)
|
|
cmd.Dir = workDir
|
|
cmd.Env = env
|
|
if b.quiet {
|
|
// Create a context with timeout to prevent hanging - use 2 hours for large packages like gcc/llvm
|
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Hour)
|
|
defer cancel()
|
|
|
|
cmd := exec.CommandContext(ctx, "sh", "-c", command)
|
|
cmd.Dir = workDir
|
|
cmd.Env = env
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
details := tailOutput(string(output), 20)
|
|
if details != "" {
|
|
return fmt.Errorf("%w\n%s", err, details)
|
|
}
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// For non-quiet mode, still set a reasonable timeout - use 2 hours for large packages
|
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Hour)
|
|
defer cancel()
|
|
cmd = exec.CommandContext(ctx, "sh", "-c", command)
|
|
cmd.Dir = workDir
|
|
cmd.Env = env
|
|
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
|
|
return cmd.Run()
|
|
}
|
|
|
|
// executeCommandWithOutput executes a command and returns output
|
|
func (b *Builder) executeCommandWithOutput(workDir, command string, env []string) (string, error) {
|
|
if strings.TrimSpace(command) == "" {
|
|
return "", nil
|
|
}
|
|
|
|
cmd := exec.Command("sh", "-c", command)
|
|
cmd.Dir = workDir
|
|
cmd.Env = env
|
|
|
|
output, err := cmd.CombinedOutput()
|
|
return string(output), err
|
|
}
|
|
|
|
// Clean removes build artifacts
|
|
func (b *Builder) Clean(recipe *recipe.Recipe) error {
|
|
dirs := []string{
|
|
recipe.GetSourceDir(b.workDir),
|
|
recipe.GetStagingDir(b.workDir),
|
|
}
|
|
|
|
for _, dir := range dirs {
|
|
if err := os.RemoveAll(dir); err != nil {
|
|
return fmt.Errorf("failed to clean directory %s: %w", dir, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetBuildInfo returns information about the build
|
|
func (b *Builder) GetBuildInfo(recipe *recipe.Recipe) (*BuildInfo, error) {
|
|
sourceDir := recipe.GetSourceDir(b.workDir)
|
|
stagingDir := recipe.GetStagingDir(b.workDir)
|
|
|
|
info := &BuildInfo{
|
|
Recipe: recipe,
|
|
SourceDir: sourceDir,
|
|
StagingDir: stagingDir,
|
|
SourceExists: false,
|
|
StagingExists: false,
|
|
}
|
|
|
|
// Check if source directory exists
|
|
if _, err := os.Stat(sourceDir); err == nil {
|
|
info.SourceExists = true
|
|
}
|
|
|
|
// Check if staging directory exists
|
|
if _, err := os.Stat(stagingDir); err == nil {
|
|
info.StagingExists = true
|
|
}
|
|
|
|
return info, nil
|
|
}
|
|
|
|
// BuildInfo contains information about a build
|
|
type BuildInfo struct {
|
|
Recipe *recipe.Recipe
|
|
SourceDir string
|
|
StagingDir string
|
|
SourceExists bool
|
|
StagingExists bool
|
|
}
|
|
|
|
// ListFilesInStaging lists all files in the staging directory
|
|
func (b *Builder) ListFilesInStaging(recipe *recipe.Recipe) ([]string, error) {
|
|
stagingDir := recipe.GetStagingDir(b.workDir)
|
|
var files []string
|
|
|
|
err := filepath.Walk(stagingDir, func(path string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !info.IsDir() {
|
|
// Convert to relative path
|
|
relPath, err := filepath.Rel(stagingDir, path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
files = append(files, relPath)
|
|
}
|
|
return nil
|
|
})
|
|
|
|
return files, err
|
|
}
|
|
|
|
// GetWorkDir returns the working directory
|
|
func (b *Builder) GetWorkDir() string {
|
|
return b.workDir
|
|
}
|
|
|
|
// SetWorkDir sets the working directory
|
|
func (b *Builder) SetWorkDir(workDir string) {
|
|
b.workDir = workDir
|
|
}
|
|
|
|
// SetQuiet enables or disables command output streaming.
|
|
func (b *Builder) SetQuiet(quiet bool) {
|
|
b.quiet = quiet
|
|
}
|
|
|
|
// SetProgressCallback sets callback for build progress updates.
|
|
func (b *Builder) SetProgressCallback(callback func(BuildProgress)) {
|
|
b.callbackMutex.Lock()
|
|
defer b.callbackMutex.Unlock()
|
|
b.progressCallback = callback
|
|
}
|
|
|
|
// SetEnvOverride sets or removes one environment override used for build commands.
|
|
func (b *Builder) SetEnvOverride(key, value string) {
|
|
key = strings.TrimSpace(key)
|
|
if key == "" {
|
|
return
|
|
}
|
|
if b.envOverrides == nil {
|
|
b.envOverrides = make(map[string]string)
|
|
}
|
|
if value == "" {
|
|
delete(b.envOverrides, key)
|
|
return
|
|
}
|
|
b.envOverrides[key] = value
|
|
}
|
|
|
|
// SetEnvOverrides replaces all environment overrides used for build commands.
|
|
func (b *Builder) SetEnvOverrides(overrides map[string]string) {
|
|
if len(overrides) == 0 {
|
|
b.envOverrides = nil
|
|
return
|
|
}
|
|
cloned := make(map[string]string, len(overrides))
|
|
for key, value := range overrides {
|
|
key = strings.TrimSpace(key)
|
|
if key == "" || value == "" {
|
|
continue
|
|
}
|
|
cloned[key] = value
|
|
}
|
|
if len(cloned) == 0 {
|
|
b.envOverrides = nil
|
|
return
|
|
}
|
|
b.envOverrides = cloned
|
|
}
|
|
|
|
// CleanCache removes cached packages and sources
|
|
func (b *Builder) CleanCache() error {
|
|
cacheDirs := []string{
|
|
filepath.Join(b.workDir, "cache"),
|
|
filepath.Join(b.workDir, "packages"),
|
|
filepath.Join(b.workDir, "sources"),
|
|
filepath.Join(b.workDir, "staging"),
|
|
}
|
|
|
|
for _, dir := range cacheDirs {
|
|
if err := os.RemoveAll(dir); err != nil {
|
|
return fmt.Errorf("failed to clean cache directory %s: %w", dir, err)
|
|
}
|
|
}
|
|
|
|
log.Printf("Cache cleaned successfully")
|
|
return nil
|
|
}
|
|
|
|
// GetCacheSize returns total size of cache directories
|
|
func (b *Builder) GetCacheSize() (int64, error) {
|
|
var totalSize int64
|
|
cacheDirs := []string{
|
|
filepath.Join(b.workDir, "cache"),
|
|
filepath.Join(b.workDir, "packages"),
|
|
filepath.Join(b.workDir, "sources"),
|
|
filepath.Join(b.workDir, "staging"),
|
|
}
|
|
|
|
for _, dir := range cacheDirs {
|
|
size, err := b.dirSize(dir)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("failed to calculate size of %s: %w", dir, err)
|
|
}
|
|
totalSize += size
|
|
}
|
|
|
|
return totalSize, nil
|
|
}
|
|
|
|
// dirSize calculates total size of directory recursively
|
|
func (b *Builder) dirSize(path string) (int64, error) {
|
|
var size int64
|
|
err := filepath.Walk(path, func(_ string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !info.IsDir() {
|
|
size += info.Size()
|
|
}
|
|
return nil
|
|
})
|
|
return size, err
|
|
}
|
|
|
|
// substituteVariables substitutes variables in command strings
|
|
func (b *Builder) substituteVariables(cmdStr, sourceDir, stagingDir string) string {
|
|
// Get number of CPU cores
|
|
jobs := runtime.NumCPU()
|
|
|
|
// Substitute variables
|
|
cmdStr = strings.ReplaceAll(cmdStr, "${jobs}", fmt.Sprintf("%d", jobs))
|
|
cmdStr = strings.ReplaceAll(cmdStr, "${pkgdir}", stagingDir)
|
|
cmdStr = strings.ReplaceAll(cmdStr, "${srcdir}", sourceDir)
|
|
cmdStr = strings.ReplaceAll(cmdStr, "{{jobs}}", fmt.Sprintf("%d", jobs))
|
|
cmdStr = strings.ReplaceAll(cmdStr, "{{pkgdir}}", stagingDir)
|
|
cmdStr = strings.ReplaceAll(cmdStr, "{{srcdir}}", sourceDir)
|
|
|
|
return cmdStr
|
|
}
|
|
|
|
// packageFiles runs the package commands
|
|
func (b *Builder) packageFiles(recipe *recipe.Recipe, sourceDir, stagingDir string, progressFn func(step, total int)) error {
|
|
// Find the source directory (usually the first subdirectory)
|
|
srcPath, err := b.findSourceDirectory(sourceDir)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to find source directory: %w", err)
|
|
}
|
|
|
|
// Set up environment
|
|
env := b.buildEnvironment(stagingDir)
|
|
|
|
// Execute package commands
|
|
for i, cmd := range recipe.Install {
|
|
if progressFn != nil {
|
|
progressFn(i+1, len(recipe.Install))
|
|
}
|
|
|
|
// Substitute variables in command
|
|
cmd = b.substituteVariables(cmd, sourceDir, stagingDir)
|
|
|
|
if err := b.executeCommand(srcPath, cmd, env); err != nil {
|
|
return fmt.Errorf("package command %d failed: %w", i+1, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (b *Builder) reportProgress(step, total int, message string) {
|
|
b.callbackMutex.RLock()
|
|
callback := b.progressCallback
|
|
b.callbackMutex.RUnlock()
|
|
|
|
if callback == nil {
|
|
return
|
|
}
|
|
callback(BuildProgress{
|
|
Step: step,
|
|
Total: total,
|
|
Message: message,
|
|
})
|
|
}
|
|
|
|
func tailOutput(output string, maxLines int) string {
|
|
output = strings.TrimSpace(output)
|
|
if output == "" {
|
|
return ""
|
|
}
|
|
|
|
lines := strings.Split(output, "\n")
|
|
if maxLines <= 0 || len(lines) <= maxLines {
|
|
return output
|
|
}
|
|
return strings.Join(lines[len(lines)-maxLines:], "\n")
|
|
}
|
|
|
|
func applyEnvOverrides(base []string, overrides map[string]string) []string {
|
|
if len(overrides) == 0 {
|
|
return base
|
|
}
|
|
|
|
out := append([]string(nil), base...)
|
|
indexByKey := make(map[string]int, len(out))
|
|
for i, entry := range out {
|
|
if eq := strings.IndexByte(entry, '='); eq > 0 {
|
|
indexByKey[entry[:eq]] = i
|
|
}
|
|
}
|
|
|
|
for key, value := range overrides {
|
|
item := key + "=" + value
|
|
if idx, ok := indexByKey[key]; ok {
|
|
out[idx] = item
|
|
continue
|
|
}
|
|
indexByKey[key] = len(out)
|
|
out = append(out, item)
|
|
}
|
|
|
|
return out
|
|
}
|
|
|
|
// validateSourceFiles checks if essential files are present after extraction
|
|
func (b *Builder) validateSourceFiles(recipe *recipe.Recipe, sourceDir string) error {
|
|
// Find the actual source directory
|
|
srcPath, err := b.findSourceDirectory(sourceDir)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to find source directory for validation: %w", err)
|
|
}
|
|
|
|
// Check if directory is not empty
|
|
entries, err := os.ReadDir(srcPath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read source directory: %w", err)
|
|
}
|
|
|
|
if len(entries) == 0 {
|
|
return fmt.Errorf("source directory is empty after extraction")
|
|
}
|
|
|
|
// ALWAYS PASS VALIDATION - trust the source
|
|
log.Printf("Source validation passed for %s: found %d files", recipe.Name, len(entries))
|
|
return nil
|
|
}
|
|
|
|
func (b *Builder) validateCommand(command string) error {
|
|
// Normalize for pattern matching
|
|
cmdLower := strings.ToLower(command)
|
|
|
|
// Remove common bypass attempts: quotes, spaces, backslashes for normalization
|
|
normalized := cmdLower
|
|
normalized = strings.ReplaceAll(normalized, `"`, "")
|
|
normalized = strings.ReplaceAll(normalized, `'`, "")
|
|
normalized = strings.ReplaceAll(normalized, ` `, "")
|
|
normalized = strings.ReplaceAll(normalized, `\`, "")
|
|
|
|
// List of dangerous patterns to block (check both original and normalized)
|
|
dangerousPatterns := []string{
|
|
"rm -rf /",
|
|
"rm -rf /*",
|
|
"rm -rf/",
|
|
`:(){:|:&};:`, // fork bomb (normalized)
|
|
":(){ :|:& };:", // fork bomb (original)
|
|
"chmod 777 /",
|
|
"chown root",
|
|
"sudo ",
|
|
"su -",
|
|
"> /dev/sda",
|
|
"> /dev/hda",
|
|
">/dev/sda",
|
|
">/dev/hda",
|
|
"mkfs",
|
|
"format /",
|
|
"fdisk",
|
|
"dd if=",
|
|
"chmod -R 777 /",
|
|
"chown -R root",
|
|
"rm -rf /etc",
|
|
"rm -rf /usr",
|
|
"rm -rf /bin",
|
|
"rm -rf /sbin",
|
|
"rm -rf /lib",
|
|
"rm -rf /lib64",
|
|
"rm-rf/etc",
|
|
"rm-rf/usr",
|
|
"rm-rf/bin",
|
|
"rm-rf/sbin",
|
|
"rm-rf/lib",
|
|
"rm-rf/lib64",
|
|
"shutdown",
|
|
"reboot",
|
|
"halt",
|
|
"poweroff",
|
|
"init 0",
|
|
"init 6",
|
|
"eval",
|
|
"$(", // command substitution
|
|
}
|
|
|
|
// Check against original
|
|
for _, pattern := range dangerousPatterns {
|
|
if strings.Contains(cmdLower, pattern) {
|
|
return fmt.Errorf("command contains potentially dangerous pattern: %s", pattern)
|
|
}
|
|
}
|
|
|
|
// Check against normalized (catches bypass attempts like 'rm -rf "/"' or 'rm -rf / ')
|
|
for _, pattern := range dangerousPatterns {
|
|
if strings.Contains(normalized, pattern) {
|
|
return fmt.Errorf("command contains potentially dangerous pattern (normalized): %s", pattern)
|
|
}
|
|
}
|
|
|
|
// Check for suspicious characters that might indicate injection
|
|
suspiciousChars := []string{"\x00", "\r", "\n", "\t"}
|
|
for _, char := range suspiciousChars {
|
|
if strings.Contains(command, char) {
|
|
return fmt.Errorf("command contains suspicious character: %q", char)
|
|
}
|
|
}
|
|
|
|
// Basic command structure validation
|
|
commands := strings.Fields(command)
|
|
if len(commands) == 0 {
|
|
return fmt.Errorf("empty command")
|
|
}
|
|
|
|
// Check for extremely long commands (possible injection attempt)
|
|
if len(command) > 1000 {
|
|
return fmt.Errorf("command too long (%d characters)", len(command))
|
|
}
|
|
|
|
// Check for command substitution patterns that could bypass security
|
|
// Backticks and $() are blocked
|
|
if strings.Contains(command, "`") {
|
|
return fmt.Errorf("command contains potentially dangerous backtick substitution")
|
|
}
|
|
if strings.Contains(command, "$(") {
|
|
return fmt.Errorf("command contains potentially dangerous command substitution $()")
|
|
}
|
|
|
|
// Log the command for audit purposes (only first 100 chars)
|
|
if len(command) > 100 {
|
|
log.Printf("Command validation passed (truncated): %s...", command[:100])
|
|
} else {
|
|
log.Printf("Command validation passed: %s", command)
|
|
}
|
|
|
|
return nil
|
|
}
|