mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-08-09 02:24:32 +00:00
180 lines
4.4 KiB
Go
180 lines
4.4 KiB
Go
package tools
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
"strings"
|
|
)
|
|
|
|
// staticCredentialEnvKeys are always stripped from fall-through exec env.
|
|
// These cover the most common provider / CLI credentials so an agent that
|
|
// runs a non-credentialed command cannot exfiltrate host secrets via the
|
|
// child process environment.
|
|
var staticCredentialEnvKeys = []string{
|
|
"GH_TOKEN",
|
|
"GITHUB_TOKEN",
|
|
"GH_ENTERPRISE_TOKEN",
|
|
"GH_CONFIG_DIR",
|
|
"ANTHROPIC_API_KEY",
|
|
"OPENAI_API_KEY",
|
|
"NPM_TOKEN",
|
|
"DOCKER_PASSWORD",
|
|
"DOCKER_AUTH",
|
|
"GOCLAW_GATEWAY_TOKEN",
|
|
"AWS_ACCESS_KEY_ID",
|
|
"AWS_SECRET_ACCESS_KEY",
|
|
"AWS_SESSION_TOKEN",
|
|
"GOOGLE_APPLICATION_CREDENTIALS",
|
|
"RAPIDAPI_KEY",
|
|
"HUGGINGFACE_TOKEN",
|
|
"HF_TOKEN",
|
|
"GCP_SA_KEY",
|
|
"AZURE_CLIENT_SECRET",
|
|
"CLOUDFLARE_API_TOKEN",
|
|
}
|
|
|
|
// scrubCredentialEnv returns env with any KEY=VALUE pair removed whose key
|
|
// matches staticCredentialEnvKeys or any key in dynamicKeys. Comparison is
|
|
// case-sensitive per POSIX — env names are case-sensitive.
|
|
func scrubCredentialEnv(env []string, dynamicKeys []string) []string {
|
|
if len(env) == 0 {
|
|
return env
|
|
}
|
|
deny := make(map[string]struct{}, len(staticCredentialEnvKeys)+len(dynamicKeys))
|
|
for _, k := range staticCredentialEnvKeys {
|
|
deny[k] = struct{}{}
|
|
}
|
|
for _, k := range dynamicKeys {
|
|
if k == "" {
|
|
continue
|
|
}
|
|
deny[k] = struct{}{}
|
|
}
|
|
out := make([]string, 0, len(env))
|
|
for _, kv := range env {
|
|
i := strings.IndexByte(kv, '=')
|
|
if i <= 0 {
|
|
out = append(out, kv)
|
|
continue
|
|
}
|
|
if _, drop := deny[kv[:i]]; drop {
|
|
continue
|
|
}
|
|
out = append(out, kv)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// credentialEnvKeys returns the union of static deny-list keys and any
|
|
// dynamic keys discovered via the secure-cli registry (encrypted_env for
|
|
// each enabled binary scoped to the caller's tenant).
|
|
//
|
|
// No caching: tenant scope comes from ctx, and a shared ExecTool instance
|
|
// serves all tenants. Caching would require a per-tenant map with
|
|
// invalidation on binary Create/Update/Delete — out of scope here (YAGNI).
|
|
// This path runs only on host fall-through exec, which is already the
|
|
// slow path. One ListEnabled query is acceptable.
|
|
//
|
|
// Fails soft: store errors fall back to the static list — env scrubbing
|
|
// never blocks exec on a DB hiccup.
|
|
func (t *ExecTool) credentialEnvKeys(ctx context.Context) []string {
|
|
if t.secureCLIStore == nil {
|
|
return staticCredentialEnvKeys
|
|
}
|
|
|
|
bins, err := t.secureCLIStore.ListEnabled(ctx)
|
|
if err != nil {
|
|
slog.Warn("security.credentialed_env_scrub_list_error", "error", err)
|
|
return staticCredentialEnvKeys
|
|
}
|
|
|
|
seen := make(map[string]struct{}, len(staticCredentialEnvKeys))
|
|
merged := make([]string, 0, len(staticCredentialEnvKeys))
|
|
for _, k := range staticCredentialEnvKeys {
|
|
if _, ok := seen[k]; ok {
|
|
continue
|
|
}
|
|
seen[k] = struct{}{}
|
|
merged = append(merged, k)
|
|
}
|
|
for _, b := range bins {
|
|
for _, k := range extractJSONTopKeys(b.EncryptedEnv) {
|
|
if _, ok := seen[k]; ok {
|
|
continue
|
|
}
|
|
seen[k] = struct{}{}
|
|
merged = append(merged, k)
|
|
}
|
|
}
|
|
return merged
|
|
}
|
|
|
|
// extractJSONTopKeys returns the top-level string keys of a JSON object
|
|
// without unmarshalling values. Tolerant: returns nil on any parse error.
|
|
// Kept local to avoid pulling json into env_scrub for a tiny helper.
|
|
func extractJSONTopKeys(data []byte) []string {
|
|
s := strings.TrimSpace(string(data))
|
|
if !strings.HasPrefix(s, "{") || !strings.HasSuffix(s, "}") {
|
|
return nil
|
|
}
|
|
var keys []string
|
|
inner := s[1 : len(s)-1]
|
|
i := 0
|
|
for i < len(inner) {
|
|
// skip whitespace + commas
|
|
for i < len(inner) && (inner[i] == ' ' || inner[i] == '\n' || inner[i] == '\t' || inner[i] == '\r' || inner[i] == ',') {
|
|
i++
|
|
}
|
|
if i >= len(inner) || inner[i] != '"' {
|
|
return keys
|
|
}
|
|
i++ // consume opening quote
|
|
start := i
|
|
for i < len(inner) && inner[i] != '"' {
|
|
if inner[i] == '\\' && i+1 < len(inner) {
|
|
i += 2
|
|
continue
|
|
}
|
|
i++
|
|
}
|
|
if i > len(inner) {
|
|
return keys
|
|
}
|
|
keys = append(keys, inner[start:i])
|
|
// advance past closing quote + optional spaces + colon + value
|
|
if i < len(inner) {
|
|
i++
|
|
}
|
|
depth := 0
|
|
inStr := false
|
|
for i < len(inner) {
|
|
c := inner[i]
|
|
if inStr {
|
|
if c == '\\' && i+1 < len(inner) {
|
|
i += 2
|
|
continue
|
|
}
|
|
if c == '"' {
|
|
inStr = false
|
|
}
|
|
i++
|
|
continue
|
|
}
|
|
switch c {
|
|
case '"':
|
|
inStr = true
|
|
case '{', '[':
|
|
depth++
|
|
case '}', ']':
|
|
depth--
|
|
}
|
|
if c == ',' && depth == 0 {
|
|
i++
|
|
break
|
|
}
|
|
i++
|
|
}
|
|
}
|
|
return keys
|
|
}
|