Full Code of hobochild/sandy for AI

master dcba083d6758 cached
10 files
8.6 KB
2.7k tokens
13 symbols
1 requests
Download .txt
Repository: hobochild/sandy
Branch: master
Commit: dcba083d6758
Files: 10
Total size: 8.6 KB

Directory structure:
gitextract_ncne_7u2/

├── .editorconfig
├── .github/
│   └── workflows/
│       └── go.yml
├── .gitignore
├── Makefile
├── README.md
├── go.mod
├── go.sum
├── password.txt
├── sandy.go
└── sandy_test.go

================================================
FILE CONTENTS
================================================

================================================
FILE: .editorconfig
================================================
[Makefile]
indent_style = tab



================================================
FILE: .github/workflows/go.yml
================================================
name: Go
on: [push]
jobs:

  build:
    name: Build
    runs-on: ubuntu-latest
    steps:

    - name: Set up Go 1.13
      uses: actions/setup-go@v1
      with:
        go-version: 1.13
      id: go

    - name: Check out code into the Go module directory
      uses: actions/checkout@v1

    - name: Get dependencies
      run: make install

    - name: Build
      run: make build


================================================
FILE: .gitignore
================================================
build
dist
sandy
.idea



================================================
FILE: Makefile
================================================
.PHONY: install 
install: go.sum
	go get -v -t

.PHONY: build 
build: install
	go build -o sandy 

.PHONY: test
test: build
	go test	

.PHONY: dist
dist:
	env GOOS=linux GOARCH=amd64 go build -o ./dist/sandy_linux_amd64


================================================
FILE: README.md
================================================
# Sandy

> A tiny sandbox to run untrusted code. 🏖️

Sandy uses Ptrace to hook into READ syscalls, giving you the option to accept or deny syscalls before they are executed.

**WARNING**: While sandy is able to intercept READ syscalls there are a variety of ways to get around this. Full details can be found in the [hackernews thread](https://news.ycombinator.com/item?id=22025986). Some of which can be patched to catch simple attacks, but you should use sandy with the expectation that it is better than nothing but it is not true isolation.

## Usage

```
Usage of ./sandy:

  sandy [FLAGS] command

  flags:
    -h	Print Usage.
    -n value
        A glob pattern for automatically blocking file reads.
    -y value
        A glob pattern for automatically allowing file reads.
```

## Use cases

### You want to install anything

```shell
> sandy -n "/etc/password.txt" npm install sketchy-module

  BLOCKED READ on /etc/password.txt
```

```shell
> sandy -n "/etc/password.txt" bash <(curl  https://danger.zone/install.sh)

  BLOCKED READ on /etc/password.txt
```

### You are interested in what file reads you favourite program makes.

Sure you could use strace, but it references file descriptors sandy makes the this much easier at a glance by printing the absolute path of the fd.

```
> sandy ls
Wanting to READ /usr/lib/x86_64-linux-gnu/libselinux.so.1 [y/n]
```

### You _don't_ want to buy your friends beer

A friend at work knows that you are security conscious and that you keep a `/free-beer.bounty` file in home directory. With the promise of a round of drinks and office wide humiliation Dave tries to trick you with a malicious script under the guise of being a helpful colleague.

You run there script with sandy and catch him red handed.

```shell
> sandy -n *.bounty bash ./dickhead-daves-script.sh

  BLOCKED READ on /free-beer.bounty
```

**NOTE**: It's definitely a better idea to encrypt all your sensitive data, sandy should probably only be used when that is inconvenient or impractical.

**NOTE**: I haven't made any effort for cross-x compatibility so it currently only works on linux. I'd happily accept patches to improve portability.


================================================
FILE: go.mod
================================================
module github.com/hobochild/sandy

go 1.13

require github.com/gobwas/glob v0.2.3


================================================
FILE: go.sum
================================================
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=


================================================
FILE: password.txt
================================================
123


================================================
FILE: sandy.go
================================================
package main

import (
	"bufio"
	"errors"
	"flag"
	"fmt"
	"github.com/gobwas/glob"
	"os"
	"os/exec"
	"strings"
	"syscall"
)

type Request struct {
	path    string
	syscall string
	allowed bool
}

func requestPermission(path string) (Request, error) {
	scanner := bufio.NewScanner(os.Stdin)
	fmt.Println(fmt.Sprintf("Wanting to READ %s [y/n]", path))
	for scanner.Scan() {
		input := strings.ToLower(scanner.Text())
		if input == "y" {
			break
		}
		if scanner.Text() == "n" {
			return Request{path, "READ", false}, nil
		}

		// Make a sounds
		fmt.Printf("\a")
	}
	return Request{path, "READ", true}, nil
}

func Exec(bin string, args, allowedPatterns, blockedPatterns []string) (map[string]Request, error) {
	var regs syscall.PtraceRegs
	reqs := make(map[string]Request)
	cmd := exec.Command(bin, args...)

	cmd.Stderr = nil
	cmd.Stdin = nil
	cmd.Stdout = nil
	cmd.SysProcAttr = &syscall.SysProcAttr{
		Ptrace: true,
		// TODO Pdeathsig a linux only
		Pdeathsig: syscall.SIGKILL,
	}
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	err := cmd.Start()
	if err != nil {
		return nil, fmt.Errorf("error while starting: %w", err)
	}
	_ = cmd.Wait()

	pid := cmd.Process.Pid

	for {
		err := syscall.PtraceGetRegs(pid, &regs)
		if err != nil {
			break
		}

		// https://stackoverflow.com/questions/33431994/extracting-system-call-name-and-arguments-using-ptrace
		if regs.Orig_rax == 0 {
			// TODO this is a cross-x barrier
			path, err := os.Readlink(fmt.Sprintf("/proc/%d/fd/%d", pid, regs.Rdi))

			if err != nil {
				return nil, err
			}

			for _, pattern := range allowedPatterns {
				g := glob.MustCompile(pattern)
				matched := g.Match(path)

				if matched {
					matchedReq := Request{path, "READ", true}
					reqs[path] = matchedReq
				}
			}

			for _, pattern := range blockedPatterns {
				g := glob.MustCompile(pattern)
				matched := g.Match(path)

				if matched {
					matchedReq := Request{path, "READ", false}
					reqs[path] = matchedReq
				}
			}

			req, ok := reqs[path]

			if !ok {
				req, err := requestPermission(path)
				if err != nil {
					return nil, err
				}
				reqs[req.path] = req

				// Throw and exit the command
				if !req.allowed {
					return nil, errors.New(fmt.Sprintf("Blocked %s on %s", req.syscall, req.path))
				}

			} else {
				// Throw and exit the command
				if !req.allowed {
					return nil, errors.New(fmt.Sprintf("Blocked %s on %s", req.syscall, req.path))
				}
			}
		}

		err = syscall.PtraceSyscall(pid, 0)
		if err != nil {
			return nil, err
		}

		_, err = syscall.Wait4(pid, nil, 0, nil)
		if err != nil {
			return nil, err
		}
	}
	return reqs, nil
}

type arrayFlags []string

func (i *arrayFlags) String() string {
	return ""
}

func (i *arrayFlags) Set(value string) error {
	*i = append(*i, value)
	return nil
}

// sandyUsage func is the usage handler for the sandy command
func sandyUsage() {
	fmt.Printf("Usage: %s [OPTIONS] command\n", os.Args[0])
	flag.PrintDefaults()
}

func main() {
	var allowedPattern arrayFlags
	var blockedPattern arrayFlags

	// TODO add sane defaults like libc etc
	allowedPattern = append(allowedPattern, "")

	// overriding the Usage handler
	flag.Usage = sandyUsage
	flag.Var(&blockedPattern, "n", "A glob pattern for automatically blocking file reads.\nFor example, \"/etc/password.txt\" or \"*.txt\".")
	flag.Var(&allowedPattern, "y", "A glob pattern for automatically allowing file reads.\nExpected format is same as -n.")
	showHelp := flag.Bool("h", false, "Print Usage.")

	flag.Parse()

	if flag.NArg() < 1  || *showHelp {
		flag.Usage()
		return
	}

	args := flag.Args()

	_, err := Exec(args[0], args[1:], allowedPattern, blockedPattern)
	if err != nil {
		fmt.Println(err)
	}
}


================================================
FILE: sandy_test.go
================================================
package main

import (
	"bytes"
	"os/exec"
	"strings"
	"testing"
)

func TestExec(t *testing.T) {
	s := []string{"password.txt"}
	patterns := []string{""}
	reqs, err := Exec("cat", s, patterns, patterns)

	if err != nil {
		t.Errorf("Something went wrong: %v", err)
	}

	if len(reqs) != 2 {
		t.Errorf("reqs count was incorrect, got: %d, want: %d.", len(reqs), 2)
	}
}

func TestInput(t *testing.T) {
	cmd := exec.Command("./sandy", "cat", "./password.txt")
	var out bytes.Buffer
	var in bytes.Buffer
	cmd.Stdout = &out
	cmd.Stdout = &in
	in.Write([]byte("n\n\r"))

	err := cmd.Run()
	if err != nil {
		t.Errorf("Something went wrong: %v", err)
	}
	if strings.Contains(out.String(), "Blocked READ on ...") {
		t.Errorf("Expected %s output got %s", "Blocked READ on ...", out.String())
	}
}

// TODO we probably should instead just pass a mock reader for stdin into the Exec function and then call the fn
// directly rather that full bin tests
func TestAllowList(t *testing.T) {
	cmd := exec.Command("./sandy", "--y", "*.so", "--y", "*.txt", "cat", "./password.txt")
	var out bytes.Buffer
	cmd.Stdout = &out
	err := cmd.Run()
	if err != nil {
		t.Errorf("Something went wrong: %v", err)
	}
	if out.String() != "123\n" {
		t.Errorf("Expected %s output got %s", "123", out.String())
	}
}

func TestBlockList(t *testing.T) {
	cmd := exec.Command("./sandy", "--y", "*.so", "--n", "*.txt", "cat", "./password.txt")
	var out bytes.Buffer
	cmd.Stdout = &out
	err := cmd.Run()
	if err != nil {
		t.Errorf("Something went wrong: %v", err)
	}
	if !strings.Contains(out.String(), "Blocked READ on ") {
		t.Errorf("Expected %s output got %s", "Blocked READ on", out.String())
	}
}

func TestHelp(t *testing.T) {
	cmd := exec.Command("./sandy", "-h")
	var out bytes.Buffer
	cmd.Stdout = &out
	err := cmd.Run()

	if err != nil {
		t.Errorf("Something went wrong: %v", err)
	}

	if strings.Contains(out.String(), "Usage of ./sandy:") {
		t.Errorf("Expected %s output got %s", "123", out.String())
	}
}
Download .txt
gitextract_ncne_7u2/

├── .editorconfig
├── .github/
│   └── workflows/
│       └── go.yml
├── .gitignore
├── Makefile
├── README.md
├── go.mod
├── go.sum
├── password.txt
├── sandy.go
└── sandy_test.go
Download .txt
SYMBOL INDEX (13 symbols across 2 files)

FILE: sandy.go
  type Request (line 15) | type Request struct
  function requestPermission (line 21) | func requestPermission(path string) (Request, error) {
  function Exec (line 39) | func Exec(bin string, args, allowedPatterns, blockedPatterns []string) (...
  type arrayFlags (line 132) | type arrayFlags
    method String (line 134) | func (i *arrayFlags) String() string {
    method Set (line 138) | func (i *arrayFlags) Set(value string) error {
  function sandyUsage (line 144) | func sandyUsage() {
  function main (line 149) | func main() {

FILE: sandy_test.go
  function TestExec (line 10) | func TestExec(t *testing.T) {
  function TestInput (line 24) | func TestInput(t *testing.T) {
  function TestAllowList (line 43) | func TestAllowList(t *testing.T) {
  function TestBlockList (line 56) | func TestBlockList(t *testing.T) {
  function TestHelp (line 69) | func TestHelp(t *testing.T) {
Condensed preview — 10 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (10K chars).
[
  {
    "path": ".editorconfig",
    "chars": 31,
    "preview": "[Makefile]\nindent_style = tab\n\n"
  },
  {
    "path": ".github/workflows/go.yml",
    "chars": 384,
    "preview": "name: Go\non: [push]\njobs:\n\n  build:\n    name: Build\n    runs-on: ubuntu-latest\n    steps:\n\n    - name: Set up Go 1.13\n  "
  },
  {
    "path": ".gitignore",
    "chars": 24,
    "preview": "build\ndist\nsandy\n.idea\n\n"
  },
  {
    "path": "Makefile",
    "chars": 220,
    "preview": ".PHONY: install \ninstall: go.sum\n\tgo get -v -t\n\n.PHONY: build \nbuild: install\n\tgo build -o sandy \n\n.PHONY: test\ntest: bu"
  },
  {
    "path": "README.md",
    "chars": 2170,
    "preview": "# Sandy\n\n> A tiny sandbox to run untrusted code. 🏖️\n\nSandy uses Ptrace to hook into READ syscalls, giving you the option"
  },
  {
    "path": "go.mod",
    "chars": 82,
    "preview": "module github.com/hobochild/sandy\n\ngo 1.13\n\nrequire github.com/gobwas/glob v0.2.3\n"
  },
  {
    "path": "go.sum",
    "chars": 163,
    "preview": "github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=\ngithub.com/gobwas/glob v0.2.3/go.mod h1:d3"
  },
  {
    "path": "password.txt",
    "chars": 4,
    "preview": "123\n"
  },
  {
    "path": "sandy.go",
    "chars": 3702,
    "preview": "package main\n\nimport (\n\t\"bufio\"\n\t\"errors\"\n\t\"flag\"\n\t\"fmt\"\n\t\"github.com/gobwas/glob\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"syscall"
  },
  {
    "path": "sandy_test.go",
    "chars": 1986,
    "preview": "package main\n\nimport (\n\t\"bytes\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestExec(t *testing.T) {\n\ts := []string{\"passwo"
  }
]

About this extraction

This page contains the full source code of the hobochild/sandy GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 10 files (8.6 KB), approximately 2.7k tokens, and a symbol index with 13 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!