Add allowlisted markdown indexing, read-only git plumbing via injected runners, and docker/readiness collection with path and loopback guards. Co-authored-by: Cursor <cursoragent@cursor.com>
110 lines
3.4 KiB
Go
110 lines
3.4 KiB
Go
package engineeringdashboard
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
var sensitivePathParts = []string{
|
|
"/.git/", "/.git", "/.env", "/.secrets/", "/.secrets", "/data/",
|
|
}
|
|
|
|
// ResolveRepoRoot returns an absolute, symlink-resolved git root.
|
|
func ResolveRepoRoot(repo string) (string, error) {
|
|
if repo == "" {
|
|
wd, err := os.Getwd()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
repo = wd
|
|
}
|
|
abs, err := filepath.Abs(repo)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
resolved, err := filepath.EvalSymlinks(abs)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
fi, err := os.Stat(resolved)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if !fi.IsDir() {
|
|
return "", fmt.Errorf("repo root is not a directory: %s", resolved)
|
|
}
|
|
return resolved, nil
|
|
}
|
|
|
|
// SafeDocPath validates a repo-relative markdown path under .doc (or allowlisted root md).
|
|
func SafeDocPath(repoRoot, rel string) (abs string, err error) {
|
|
rel = filepath.ToSlash(rel)
|
|
rel = strings.TrimPrefix(rel, "./")
|
|
if rel == "" || strings.Contains(rel, "\x00") {
|
|
return "", fmt.Errorf("empty path")
|
|
}
|
|
if filepath.IsAbs(rel) || strings.HasPrefix(rel, "/") {
|
|
return "", fmt.Errorf("absolute path denied: %s", rel)
|
|
}
|
|
if strings.Contains(rel, "..") {
|
|
return "", fmt.Errorf("path traversal denied: %s", rel)
|
|
}
|
|
allowRootMD := !strings.Contains(rel, "/") && strings.HasSuffix(strings.ToLower(rel), ".md")
|
|
underDoc := strings.HasPrefix(rel, ".doc/")
|
|
if !underDoc && !allowRootMD {
|
|
return "", fmt.Errorf("path outside allowlist: %s", rel)
|
|
}
|
|
if strings.Contains(rel, "/assets/") || strings.HasSuffix(rel, "/assets") {
|
|
return "", fmt.Errorf("assets body denied: %s", rel)
|
|
}
|
|
joined := filepath.Join(repoRoot, filepath.FromSlash(rel))
|
|
// Reject if the path itself is a symlink (before resolution).
|
|
fi, err := os.Lstat(joined)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if fi.Mode()&os.ModeSymlink != 0 {
|
|
return "", fmt.Errorf("symlink denied: %s", rel)
|
|
}
|
|
resolved, err := filepath.EvalSymlinks(joined)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
repoResolved, err := filepath.EvalSymlinks(repoRoot)
|
|
if err != nil {
|
|
repoResolved = repoRoot
|
|
}
|
|
if !strings.HasPrefix(resolved, repoResolved+string(os.PathSeparator)) && resolved != repoResolved {
|
|
return "", fmt.Errorf("resolved path escapes repo: %s", rel)
|
|
}
|
|
lower := filepath.ToSlash(strings.ToLower(resolved))
|
|
for _, part := range sensitivePathParts {
|
|
if strings.Contains(lower, part) || strings.HasSuffix(lower, strings.TrimSuffix(part, "/")) {
|
|
// allow .doc paths that happen to mention words; only block real sensitive dirs
|
|
if strings.Contains(lower, "/.git") || strings.Contains(lower, "/.env") ||
|
|
strings.Contains(lower, "/.secrets") || strings.Contains(lower, "/data/") {
|
|
if !strings.Contains(filepath.ToSlash(rel), ".doc/") {
|
|
return "", fmt.Errorf("sensitive path denied: %s", rel)
|
|
}
|
|
// still deny .git/.env/.secrets even under mistaken .doc
|
|
if strings.Contains(lower, "/.git") || strings.Contains(lower, "/.secrets") ||
|
|
strings.Contains(filepath.Base(lower), ".env") {
|
|
return "", fmt.Errorf("sensitive path denied: %s", rel)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if !strings.HasSuffix(strings.ToLower(resolved), ".md") {
|
|
return "", fmt.Errorf("extension not allowed: %s", rel)
|
|
}
|
|
return resolved, nil
|
|
}
|
|
|
|
// IsAssetsPath reports whether a relative path is under an assets directory.
|
|
func IsAssetsPath(rel string) bool {
|
|
rel = filepath.ToSlash(rel)
|
|
return strings.Contains(rel, "/assets/") || strings.HasSuffix(rel, "/assets") || strings.HasPrefix(rel, "assets/")
|
|
}
|