Assistant: codex Assistant-Model: gpt-6.1-sol Assistant-Session: 01a14bfc-9643-7872-aa12-98326dcf25e7
192 lines
6.3 KiB
Go
192 lines
6.3 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"errors"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
"syscall"
|
|
)
|
|
|
|
// Same bounded small-input profile as Repo Manager's staged adapter. Above
|
|
// PIPE_BUF, keep the original stdin reader/fragment path byte-for-byte.
|
|
const directoryInputBytes = 4096
|
|
const directoryBatchInputs = 64
|
|
const maxReportBytes = 1024 * 1024
|
|
|
|
type scanInput struct {
|
|
name string
|
|
content []byte
|
|
}
|
|
|
|
func verifyScannerFD(f *os.File) error {
|
|
st, err := f.Stat()
|
|
if err != nil {
|
|
return errors.New("scanner-file-refused")
|
|
}
|
|
native, ok := st.Sys().(*syscall.Stat_t)
|
|
if !ok || !st.Mode().IsRegular() || st.Mode().Perm()&022 != 0 || st.Mode().Perm()&0111 == 0 || st.Size() > 64*1024*1024 || (native.Uid != 0 && native.Uid != uint32(os.Getuid())) {
|
|
return errors.New("scanner-file-refused")
|
|
}
|
|
if _, err = f.Seek(0, 0); err != nil {
|
|
return errors.New("scanner-file-refused")
|
|
}
|
|
hash := sha256.New()
|
|
if _, err = io.Copy(hash, f); err != nil || hex.EncodeToString(hash.Sum(nil)) != scannerHash {
|
|
return errors.New("scanner-digest-mismatch")
|
|
}
|
|
if _, err = f.Seek(0, 0); err != nil {
|
|
return errors.New("scanner-file-refused")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Trusted child mode establishes resource limits before exec of the exact
|
|
// independently pinned scanner descriptor. It accepts no repository commands.
|
|
func batchScannerProcess(args []string) int {
|
|
if len(args) != 2 {
|
|
return 127
|
|
}
|
|
if _, err := volatileIdentity(args[1]); err != nil {
|
|
return 127
|
|
}
|
|
f := os.NewFile(3, "pinned-scanner")
|
|
if f == nil || verifyScannerFD(f) != nil {
|
|
return 127
|
|
}
|
|
defer f.Close()
|
|
for resource, limit := range map[int]uint64{syscall.RLIMIT_FSIZE: maxReportBytes, syscall.RLIMIT_CPU: 20, syscall.RLIMIT_AS: 8 * 1024 * 1024 * 1024} {
|
|
if syscall.Setrlimit(resource, &syscall.Rlimit{Cur: limit, Max: limit}) != nil {
|
|
return 127
|
|
}
|
|
}
|
|
if os.Chdir(args[1]) != nil {
|
|
return 127
|
|
}
|
|
argv := []string{"gitleaks", "dir", "--config", filepath.Join(args[0], "scanner.toml"), "--gitleaks-ignore-path", filepath.Join(args[0], "ignore"), "--ignore-gitleaks-allow", "--redact=100", "--no-banner", "--no-color", "--log-level", "warn", "--exit-code", "20", "--timeout", "15", "--report-format", "template", "--report-template", filepath.Join(args[0], "template"), "--report-path", filepath.Join(args[0], "report.paths"), "--max-target-megabytes", "0", "payload"}
|
|
if syscall.Exec("/proc/self/fd/3", argv, []string{"PATH=/usr/bin:/bin", "HOME=" + args[1], "TMPDIR=" + args[1], "LANG=C"}) != nil {
|
|
return 127
|
|
}
|
|
return 127
|
|
}
|
|
|
|
var safeInputName = regexp.MustCompile(`^(?:[0-9a-f]{40}|[0-9a-f]{64}|ref-[0-9]{1,3})$`)
|
|
var leakSummary = regexp.MustCompile(`^[0-9]{1,2}:[0-9]{2}(?:AM|PM) WRN leaks found: [1-9][0-9]*\n$`)
|
|
|
|
func validateBatchDiagnostics(code int, diagnostics []byte) error {
|
|
if code != 0 && code != 20 || len(diagnostics) > 0 && !(code == 20 && leakSummary.Match(diagnostics)) {
|
|
return errors.New("scanner-failed")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func batchFindings(code int, raw []byte, known map[string]string) (map[string]bool, error) {
|
|
findings := map[string]bool{}
|
|
if len(raw) > 0 {
|
|
if !bytes.HasSuffix(raw, []byte("\n")) {
|
|
return nil, errors.New("scanner-report-refused")
|
|
}
|
|
for _, line := range strings.Split(string(raw[:len(raw)-1]), "\n") {
|
|
name, ok := known[line]
|
|
if !ok {
|
|
return nil, errors.New("scanner-report-refused")
|
|
}
|
|
findings[name] = true
|
|
}
|
|
}
|
|
if (code != 0 && code != 20) || (len(findings) > 0) != (code == 20) {
|
|
return nil, errors.New("scanner-report-refused")
|
|
}
|
|
return findings, nil
|
|
}
|
|
|
|
func scanSmall(ctx context.Context, scanner *os.File, config string, storage *volatileStorage, inputs []scanInput) (blocked string, err error) {
|
|
if len(inputs) == 0 || len(inputs) > directoryBatchInputs || !storage.live() {
|
|
return "", errors.New("cleanup-watchdog-unavailable")
|
|
}
|
|
id, e := volatileIdentity(storage.path)
|
|
if e != nil || id != storage.id {
|
|
return "", errors.New("volatile-storage-identity-changed")
|
|
}
|
|
payload := filepath.Join(storage.path, "payload")
|
|
if os.Mkdir(payload, 0700) != nil {
|
|
return "", errors.New("volatile-storage-unavailable")
|
|
}
|
|
defer func() {
|
|
if os.RemoveAll(payload) != nil && err == nil {
|
|
err = errors.New("volatile-storage-cleanup-failed")
|
|
}
|
|
}()
|
|
known := map[string]string{}
|
|
for _, item := range inputs {
|
|
if !safeInputName.MatchString(item.name) || len(item.content) > directoryInputBytes || known["payload/"+item.name] != "" {
|
|
return "", errors.New("invalid-scan-batch")
|
|
}
|
|
known["payload/"+item.name] = item.name
|
|
file, e := os.OpenFile(filepath.Join(payload, item.name), os.O_WRONLY|os.O_CREATE|os.O_EXCL|syscall.O_NOFOLLOW, 0600)
|
|
if e != nil {
|
|
return "", errors.New("volatile-storage-write-failed")
|
|
}
|
|
_, e = file.Write(item.content)
|
|
closed := file.Close()
|
|
if e != nil || closed != nil {
|
|
return "", errors.New("volatile-storage-write-failed")
|
|
}
|
|
}
|
|
if os.WriteFile(filepath.Join(config, "template"), []byte("{{range .}}{{.File}}\n{{end}}"), 0600) != nil {
|
|
return "", errors.New("scanner-config-unavailable")
|
|
}
|
|
report := filepath.Join(config, "report.paths")
|
|
if e = os.Remove(report); e != nil && !os.IsNotExist(e) {
|
|
return "", errors.New("scanner-report-refused")
|
|
}
|
|
cmd := exec.CommandContext(ctx, "/proc/self/exe", "--batch-scanner", config, storage.path)
|
|
cmd.ExtraFiles = []*os.File{scanner}
|
|
cmd.Env = []string{"PATH=/usr/bin:/bin", "LANG=C"}
|
|
diagnostics := &cappedBuffer{limit: maxReportBytes}
|
|
cmd.Stdout = io.Discard
|
|
cmd.Stderr = diagnostics
|
|
runErr := runChild(cmd)
|
|
if ctx.Err() != nil {
|
|
return "", errors.New("scan-deadline-exceeded")
|
|
}
|
|
code := 0
|
|
var exit *exec.ExitError
|
|
if runErr != nil {
|
|
if !errors.As(runErr, &exit) {
|
|
return "", errors.New("scanner-failed")
|
|
}
|
|
code = exit.ExitCode()
|
|
}
|
|
if e = validateBatchDiagnostics(code, diagnostics.Bytes()); e != nil {
|
|
return "", e
|
|
}
|
|
st, e := os.Lstat(report)
|
|
if e != nil || !st.Mode().IsRegular() || st.Size() > maxReportBytes {
|
|
return "", errors.New("scanner-report-refused")
|
|
}
|
|
raw, e := os.ReadFile(report)
|
|
if e != nil {
|
|
return "", errors.New("scanner-report-refused")
|
|
}
|
|
findings, e := batchFindings(code, raw, known)
|
|
if e != nil {
|
|
return "", e
|
|
}
|
|
if !storage.live() {
|
|
return "", errors.New("scanner-report-refused")
|
|
}
|
|
for _, item := range inputs {
|
|
if findings[item.name] {
|
|
return item.name, nil
|
|
}
|
|
}
|
|
return "", nil
|
|
}
|