Full Code of liamg/siphon for AI

main 6a6e402ef506 cached
15 files
11.8 KB
4.4k tokens
12 symbols
1 requests
Download .txt
Repository: liamg/siphon
Branch: main
Commit: 6a6e402ef506
Files: 15
Total size: 11.8 KB

Directory structure:
gitextract_i7w6ueu6/

├── .github/
│   └── workflows/
│       ├── release.yml
│       └── test.yml
├── .gitignore
├── .goreleaser.yml
├── LICENSE
├── README.md
├── go.mod
├── go.sum
├── intercept.go
├── main.go
├── registers_386.go
├── registers_amd64.go
├── registers_arm.go
├── registers_arm64.go
└── watch_test.go

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

================================================
FILE: .github/workflows/release.yml
================================================
name: release

on:
  push:
    tags:
      - v*

jobs:
  build:
    name: releasing
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 0

      - uses: actions/setup-go@v3
        with:
          go-version: "1.19"
      - uses: goreleaser/goreleaser-action@v3
        with:
          version: latest
          args: release --rm-dist
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

================================================
FILE: .github/workflows/test.yml
================================================
name: tests
on:
  pull_request:
jobs:
  test:
    name: tests
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ ubuntu-latest ]

    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-go@v3
        with:
          go-version: '1.19'
          cache: true
      - name: Run tests
        run: sudo -E $(which go) test ./...


================================================
FILE: .gitignore
================================================
.idea


================================================
FILE: .goreleaser.yml
================================================
builds:
  - id: siphon
    main: .
    binary: siphon
    ldflags:
      - "-s -w -extldflags '-fno-PIC -static'"
    env:
      - CGO_ENABLED=0
    goos:
      - linux
    goarch:
      - "amd64"
      - "arm64"
      - "386"
      - "arm"
changelog:
  sort: asc
  filters:
    exclude:
      - "^docs:"
      - "^test:"

archives:
  - format: binary
    name_template: "{{ .Binary}}-{{ .Os }}-{{ .Arch }}"

release:
  prerelease: auto
  github:
    owner: liamg
    name: siphon

================================================
FILE: LICENSE
================================================
This is free and unencumbered software released into the public domain.

Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.

In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.

For more information, please refer to <http://unlicense.org/>


================================================
FILE: README.md
================================================
# Siphon

Intercept input/output (_stdin/stdout/stderr_) for any process, even where said output is sent to _/dev/null_ or elsewhere.

![demo gif](demo.gif)

It can also be used to spy on another users shell:

![demo gif 2](demo2.gif)

Currently Siphon works on Linux, with `amd64`, `arm64`, `arm`, and `386`. Adding support for more architectures is pretty simple, feel free to raise an issue.

It uses `ptrace` which means you'll likely need to run it as `root` for the ptrace privilege.

## Installation

Grab a binary from the [latest release](https://github.com/liamg/siphon/releases/latest).


================================================
FILE: go.mod
================================================
module github.com/liamg/siphon

go 1.19

require (
	github.com/spf13/cobra v1.6.0
	github.com/stretchr/testify v1.7.4
)

require (
	github.com/davecgh/go-spew v1.1.1 // indirect
	github.com/inconshreveable/mousetrap v1.0.1 // indirect
	github.com/pmezard/go-difflib v1.0.0 // indirect
	github.com/spf13/pflag v1.0.5 // indirect
	gopkg.in/yaml.v3 v3.0.1 // indirect
)


================================================
FILE: go.sum
================================================
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.6.0 h1:42a0n6jwCot1pUmomAp4T7DeMD+20LFv4Q54pxLf2LI=
github.com/spf13/cobra v1.6.0/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.4 h1:wZRexSlwd7ZXfKINDLsO4r7WBt3gTKONc6K/VesHvHM=
github.com/stretchr/testify v1.7.4/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=


================================================
FILE: intercept.go
================================================
package main

import (
	"fmt"
	"os"
	"runtime"
	"syscall"
)

func watchProcess(pid int, stdout, stderr, stdin bool) error {
	// ensure tracing all comes from same thread
	runtime.LockOSThread()

	if _, err := os.FindProcess(pid); err != nil {
		return fmt.Errorf("could not find process with pid %d: %w", pid, err)
	}

	if err := syscall.PtraceAttach(pid); err == syscall.EPERM {
		return fmt.Errorf("could not attach to process with pid %d: %w - check your permissions", pid, err)
	} else if err != nil {
		return err
	}

	status := syscall.WaitStatus(0)
	if _, err := syscall.Wait4(pid, &status, 0, nil); err != nil {
		return err
	}

	defer func() {
		_ = syscall.PtraceDetach(pid)
		_, _ = syscall.Wait4(pid, &status, 0, nil)
	}()

	// deliver SIGTRAP|0x80
	if err := syscall.PtraceSetOptions(pid, syscall.PTRACE_O_TRACESYSGOOD); err != nil {
		return err
	}

	for {
		fd, data, err := interceptReadsAndWrites(pid)
		if err != nil {
			return err
		}

		if stdout && fd == uint64(syscall.Stdout) || stderr && fd == uint64(syscall.Stderr) || stdin && fd == uint64(syscall.Stdin) {
			if fd == uint64(syscall.Stdin) {
				fd = uint64(os.Stdin.Fd())
			}
			_, _ = fmt.Fprintf(os.NewFile(uintptr(fd), "pipe"), "%s", string(data))
		}
	}
}

func interceptReadsAndWrites(pid int) (fd uint64, data []byte, err error) {

	// intercept syscall
	err = syscall.PtraceSyscall(pid, 0)
	if err != nil {
		return 0, nil, fmt.Errorf("could not intercept syscall: %w", err)
	}

	// wait for a syscall
	status := syscall.WaitStatus(0)
	_, err = syscall.Wait4(pid, &status, 0, nil)
	if err != nil {
		return 0, nil, err
	}

	// if interrupted, stop tracing
	if status.StopSignal().String() == "interrupt" {
		_ = syscall.PtraceSyscall(pid, int(status.StopSignal()))
		return 0, nil, fmt.Errorf("process interrupted")
	}

	var exited bool

	waitForExit := func() error {

		if exited {
			return nil
		}
		exited = true

		// continue the syscall we intercepted
		err = syscall.PtraceSyscall(pid, 0)
		if err != nil {
			return fmt.Errorf("could not continue process: %w", err)
		}

		// and wait for it to finish
		status = syscall.WaitStatus(0)
		_, err = syscall.Wait4(pid, &status, 0, nil)
		if err != nil {
			return err
		}

		return nil
	}

	defer func() {
		err = waitForExit()
		if err == nil {
			// process exited
			if status.Exited() {
				err = fmt.Errorf("process exited")
				return
			}

			// if interrupted, stop tracing
			if status.StopSignal().String() == "interrupt" {
				_ = syscall.PtraceSyscall(pid, int(status.StopSignal()))
				err = fmt.Errorf("process interrupted")
				return
			}
		}
	}()

	// if we have a syscall, examine it...
	if status.TrapCause()&int(syscall.SIGTRAP|0x80) > 0 {

		// wait for syscall exit
		if err := waitForExit(); err != nil {
			return 0, nil, err
		}

		// read registers
		regs := &syscall.PtraceRegs{}
		if err := syscall.PtraceGetRegs(pid, regs); err != nil {
			return 0, nil, err
		}

		// find the syscall number for the host architecture
		syscallNo := grabSyscallNo(regs)

		// if it's a read/write syscall, grab the args
		switch syscallNo {
		case syscall.SYS_READ, syscall.SYS_WRITE:

			// grab the args to WRITE for the host architecture
			// fd == file descriptor (generally 1 for stdout, 2 for stderr)
			// ptr == pointer to the buffer
			// lng == length of the buffer
			fd, ptr, lng := grabArgsFromRegs(regs)

			// if we want to see this output, read it from memory
			if lng > 0 {
				data := make([]byte, lng)
				if _, err := syscall.PtracePeekData(pid, uintptr(ptr), data); err != nil {
					return 0, nil, err
				}
				return fd, data, nil
			}
		}

	}

	return 0, nil, err
}


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

import (
	"fmt"
	"strconv"

	"github.com/spf13/cobra"
)

var cmd = &cobra.Command{
	Use:  "siphon [pid]",
	Args: cobra.ExactArgs(1),
	RunE: func(cmd *cobra.Command, args []string) error {
		cmd.SilenceUsage = true
		cmd.SilenceErrors = true
		pid, err := strconv.ParseInt(args[0], 10, 64)
		if err != nil {
			return fmt.Errorf("invalid pid: %w", err)
		}
		return watchProcess(int(pid), flagStdOut, flagStdErr, flagStdIn)
	},
}

var (
	flagStdOut = true
	flagStdErr = true
	flagStdIn  = false
)

func main() {

	cmd.Flags().BoolVarP(&flagStdOut, "stdout", "o", flagStdOut, "Show stdout")
	cmd.Flags().BoolVarP(&flagStdErr, "stderr", "e", flagStdErr, "Show stderr")
	cmd.Flags().BoolVarP(&flagStdIn, "stdin", "i", flagStdIn, "Show stdin")

	if err := cmd.Execute(); err != nil {
		_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Error: %v", err)
	}
}


================================================
FILE: registers_386.go
================================================
//go:build 386

package main

import (
	"syscall"
)

func grabSyscallNo(regs *syscall.PtraceRegs) uint64 {
	return uint64(regs.Orig_eax)
}

func grabArgsFromRegs(regs *syscall.PtraceRegs) (fd, ptr, lng uint64) {
	return uint64(regs.Ebx), uint64(regs.Ecx), uint64(regs.Edx)
}


================================================
FILE: registers_amd64.go
================================================
//go:build amd64

package main

import (
	"syscall"
)

func grabSyscallNo(regs *syscall.PtraceRegs) uint64 {
	return regs.Orig_rax
}

func grabArgsFromRegs(regs *syscall.PtraceRegs) (fd, ptr, lng uint64) {
	return regs.Rdi, regs.Rsi, regs.Rdx
}


================================================
FILE: registers_arm.go
================================================
//go:build arm

package main

import (
	"syscall"
)

func grabSyscallNo(regs *syscall.PtraceRegs) uint64 {
	return uint64(regs.Uregs[7])
}

func grabArgsFromRegs(regs *syscall.PtraceRegs) (fd, ptr, lng uint64) {
	return uint64(regs.Uregs[0]), uint64(regs.Uregs[1]), uint64(regs.Uregs[2])
}


================================================
FILE: registers_arm64.go
================================================
//go:build arm64

package main

import (
	"syscall"
)

func grabSyscallNo(regs *syscall.PtraceRegs) uint64 {
	return regs.Regs[8]
}

func grabArgsFromRegs(regs *syscall.PtraceRegs) (fd, ptr, lng uint64) {
	return regs.Regs[0], regs.Regs[1], regs.Regs[2]
}


================================================
FILE: watch_test.go
================================================
package main

import (
	"bytes"
	"os"
	"os/exec"
	"strconv"
	"strings"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"

	"github.com/stretchr/testify/require"
)

func Test_Watch(t *testing.T) {

	require.Equal(t, 0, os.Getuid(), "test must be run as root")

	pingBuffer := bytes.NewBuffer([]byte{})
	pingCmd := exec.Command("ping", "127.0.0.1")
	pingCmd.Stdout = pingBuffer
	require.NoError(t, pingCmd.Start())

	time.Sleep(time.Second)

	watchBuffer := bytes.NewBuffer([]byte{})

	watchCmd := exec.Command("go", "run", ".", strconv.Itoa(pingCmd.Process.Pid))
	watchCmd.Stdout = watchBuffer
	require.NoError(t, watchCmd.Start())

	time.Sleep(5 * time.Second)
	require.NoError(t, pingCmd.Process.Kill())
	require.NoError(t, pingCmd.Process.Release())

	_ = watchCmd.Wait()

	watchOutput := watchBuffer.String()
	assert.True(t, strings.HasSuffix(pingBuffer.String(), watchOutput))
	assert.Greater(t, len(watchOutput), 0)
}
Download .txt
gitextract_i7w6ueu6/

├── .github/
│   └── workflows/
│       ├── release.yml
│       └── test.yml
├── .gitignore
├── .goreleaser.yml
├── LICENSE
├── README.md
├── go.mod
├── go.sum
├── intercept.go
├── main.go
├── registers_386.go
├── registers_amd64.go
├── registers_arm.go
├── registers_arm64.go
└── watch_test.go
Download .txt
SYMBOL INDEX (12 symbols across 7 files)

FILE: intercept.go
  function watchProcess (line 10) | func watchProcess(pid int, stdout, stderr, stdin bool) error {
  function interceptReadsAndWrites (line 54) | func interceptReadsAndWrites(pid int) (fd uint64, data []byte, err error) {

FILE: main.go
  function main (line 30) | func main() {

FILE: registers_386.go
  function grabSyscallNo (line 9) | func grabSyscallNo(regs *syscall.PtraceRegs) uint64 {
  function grabArgsFromRegs (line 13) | func grabArgsFromRegs(regs *syscall.PtraceRegs) (fd, ptr, lng uint64) {

FILE: registers_amd64.go
  function grabSyscallNo (line 9) | func grabSyscallNo(regs *syscall.PtraceRegs) uint64 {
  function grabArgsFromRegs (line 13) | func grabArgsFromRegs(regs *syscall.PtraceRegs) (fd, ptr, lng uint64) {

FILE: registers_arm.go
  function grabSyscallNo (line 9) | func grabSyscallNo(regs *syscall.PtraceRegs) uint64 {
  function grabArgsFromRegs (line 13) | func grabArgsFromRegs(regs *syscall.PtraceRegs) (fd, ptr, lng uint64) {

FILE: registers_arm64.go
  function grabSyscallNo (line 9) | func grabSyscallNo(regs *syscall.PtraceRegs) uint64 {
  function grabArgsFromRegs (line 13) | func grabArgsFromRegs(regs *syscall.PtraceRegs) (fd, ptr, lng uint64) {

FILE: watch_test.go
  function Test_Watch (line 17) | func Test_Watch(t *testing.T) {
Condensed preview — 15 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (14K chars).
[
  {
    "path": ".github/workflows/release.yml",
    "chars": 458,
    "preview": "name: release\n\non:\n  push:\n    tags:\n      - v*\n\njobs:\n  build:\n    name: releasing\n    runs-on: ubuntu-latest\n\n    step"
  },
  {
    "path": ".github/workflows/test.yml",
    "chars": 363,
    "preview": "name: tests\non:\n  pull_request:\njobs:\n  test:\n    name: tests\n    runs-on: ${{ matrix.os }}\n    strategy:\n      matrix:\n"
  },
  {
    "path": ".gitignore",
    "chars": 6,
    "preview": ".idea\n"
  },
  {
    "path": ".goreleaser.yml",
    "chars": 480,
    "preview": "builds:\n  - id: siphon\n    main: .\n    binary: siphon\n    ldflags:\n      - \"-s -w -extldflags '-fno-PIC -static'\"\n    en"
  },
  {
    "path": "LICENSE",
    "chars": 1211,
    "preview": "This is free and unencumbered software released into the public domain.\n\nAnyone is free to copy, modify, publish, use, c"
  },
  {
    "path": "README.md",
    "chars": 598,
    "preview": "# Siphon\n\nIntercept input/output (_stdin/stdout/stderr_) for any process, even where said output is sent to _/dev/null_ "
  },
  {
    "path": "go.mod",
    "chars": 367,
    "preview": "module github.com/liamg/siphon\n\ngo 1.19\n\nrequire (\n\tgithub.com/spf13/cobra v1.6.0\n\tgithub.com/stretchr/testify v1.7.4\n)\n"
  },
  {
    "path": "go.sum",
    "chars": 2050,
    "preview": "github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=\ngithub.com/davecgh/go-spe"
  },
  {
    "path": "intercept.go",
    "chars": 3649,
    "preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"runtime\"\n\t\"syscall\"\n)\n\nfunc watchProcess(pid int, stdout, stderr, stdin bool) erro"
  },
  {
    "path": "main.go",
    "chars": 856,
    "preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\n\t\"github.com/spf13/cobra\"\n)\n\nvar cmd = &cobra.Command{\n\tUse:  \"siphon [pid]\",\n"
  },
  {
    "path": "registers_386.go",
    "chars": 275,
    "preview": "//go:build 386\n\npackage main\n\nimport (\n\t\"syscall\"\n)\n\nfunc grabSyscallNo(regs *syscall.PtraceRegs) uint64 {\n\treturn uint6"
  },
  {
    "path": "registers_amd64.go",
    "chars": 245,
    "preview": "//go:build amd64\n\npackage main\n\nimport (\n\t\"syscall\"\n)\n\nfunc grabSyscallNo(regs *syscall.PtraceRegs) uint64 {\n\treturn reg"
  },
  {
    "path": "registers_arm.go",
    "chars": 290,
    "preview": "//go:build arm\n\npackage main\n\nimport (\n\t\"syscall\"\n)\n\nfunc grabSyscallNo(regs *syscall.PtraceRegs) uint64 {\n\treturn uint6"
  },
  {
    "path": "registers_arm64.go",
    "chars": 256,
    "preview": "//go:build arm64\n\npackage main\n\nimport (\n\t\"syscall\"\n)\n\nfunc grabSyscallNo(regs *syscall.PtraceRegs) uint64 {\n\treturn reg"
  },
  {
    "path": "watch_test.go",
    "chars": 931,
    "preview": "package main\n\nimport (\n\t\"bytes\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strconv\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify"
  }
]

About this extraction

This page contains the full source code of the liamg/siphon GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 15 files (11.8 KB), approximately 4.4k tokens, and a symbol index with 12 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!