a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
206 lines
6.2 KiB
Go
206 lines
6.2 KiB
Go
package contracts
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/zzet/gortex/internal/graph"
|
|
)
|
|
|
|
// EnvVarExtractor detects environment variable reads (consumers) and definitions (providers).
|
|
type EnvVarExtractor struct{}
|
|
|
|
var (
|
|
// Consumer patterns.
|
|
envConsumerPatterns = []*regexp.Regexp{
|
|
// Go: os.Getenv("VAR"), os.LookupEnv("VAR")
|
|
regexp.MustCompile(`os\.Getenv\(\s*"([^"]+)"`),
|
|
regexp.MustCompile(`os\.LookupEnv\(\s*"([^"]+)"`),
|
|
// TS: process.env.VAR
|
|
regexp.MustCompile(`process\.env\.([A-Z][A-Z0-9_]+)`),
|
|
// TS: process.env["VAR"]
|
|
regexp.MustCompile(`process\.env\[\s*"([^"]+)"\s*\]`),
|
|
// Python: os.environ["VAR"], os.getenv("VAR")
|
|
regexp.MustCompile(`os\.environ\[\s*"([^"]+)"\s*\]`),
|
|
regexp.MustCompile(`os\.getenv\(\s*"([^"]+)"`),
|
|
// Java: System.getenv("VAR")
|
|
regexp.MustCompile(`System\.getenv\(\s*"([^"]+)"`),
|
|
}
|
|
|
|
// Provider patterns.
|
|
envProviderPatterns = []*regexp.Regexp{
|
|
// Go: os.Setenv("VAR", value)
|
|
regexp.MustCompile(`os\.Setenv\(\s*"([^"]+)"`),
|
|
}
|
|
|
|
// .env file lines: VAR=value
|
|
envFileLineRe = regexp.MustCompile(`(?m)^([A-Z][A-Z0-9_]+)\s*=`)
|
|
|
|
// docker-compose environment entries: - VAR=value or VAR: value
|
|
dockerEnvRe = regexp.MustCompile(`(?m)^\s*-?\s*([A-Z][A-Z0-9_]+)\s*[=:]`)
|
|
)
|
|
|
|
func (e *EnvVarExtractor) SupportedLanguages() []string {
|
|
return []string{"go", "typescript", "javascript", "python", "java", "env", "yaml"}
|
|
}
|
|
|
|
// envCodePrefilterMarkers covers every code-path regex (consumer
|
|
// + provider). A file must contain at least one of these before we
|
|
// run the seven consumer regexes and one provider regex — typical
|
|
// .go/.ts source files without env-var access short-circuit here.
|
|
// Env files and docker-compose are routed to separate extractors
|
|
// before this check.
|
|
var envCodePrefilterMarkers = [][]byte{
|
|
[]byte("Getenv"), // os.Getenv / System.getenv (both capitalised)
|
|
[]byte("getenv"), // python os.getenv / lowercase variants
|
|
[]byte("LookupEnv"), // os.LookupEnv
|
|
[]byte("Setenv"), // os.Setenv provider
|
|
[]byte("process.env"), // TS/JS
|
|
[]byte("os.environ"), // python
|
|
}
|
|
|
|
func (e *EnvVarExtractor) Extract(filePath string, src []byte, nodes []*graph.Node, edges []*graph.Edge) []Contract {
|
|
var contracts []Contract
|
|
|
|
if isEnvFile(filePath) {
|
|
contracts = append(contracts, e.extractEnvFileProviders(filePath, src)...)
|
|
return contracts
|
|
}
|
|
if isDockerCompose(filePath) {
|
|
contracts = append(contracts, e.extractDockerComposeProviders(filePath, src)...)
|
|
return contracts
|
|
}
|
|
|
|
if !srcHasAnyMarker(src, envCodePrefilterMarkers) {
|
|
return nil
|
|
}
|
|
|
|
// Thread file nodes into consumer extraction so each os.Getenv
|
|
// site anchors on its enclosing function — required for tools
|
|
// like get_dependencies(handler) to trace which env vars a
|
|
// request needs read.
|
|
fileNodes := filterFileNodes(filePath, nodes)
|
|
sort.Slice(fileNodes, func(i, j int) bool {
|
|
return fileNodes[i].StartLine < fileNodes[j].StartLine
|
|
})
|
|
contracts = append(contracts, e.extractCodeConsumers(filePath, src, fileNodes)...)
|
|
contracts = append(contracts, e.extractCodeProviders(filePath, src)...)
|
|
|
|
return contracts
|
|
}
|
|
|
|
func (e *EnvVarExtractor) extractCodeConsumers(filePath string, src []byte, fileNodes []*graph.Node) []Contract {
|
|
var contracts []Contract
|
|
text := string(src)
|
|
lines := strings.Split(text, "\n")
|
|
|
|
for _, re := range envConsumerPatterns {
|
|
for _, m := range re.FindAllStringSubmatchIndex(text, -1) {
|
|
varName := text[m[2]:m[3]]
|
|
ln := lineNumber(lines, m[0])
|
|
contracts = append(contracts, Contract{
|
|
ID: fmt.Sprintf("env::%s", varName),
|
|
Type: ContractEnv,
|
|
Role: RoleConsumer,
|
|
SymbolID: findEnclosingSymbol(fileNodes, ln),
|
|
FilePath: filePath,
|
|
Line: ln,
|
|
Meta: map[string]any{"var": varName},
|
|
Confidence: 0.9,
|
|
})
|
|
}
|
|
}
|
|
|
|
return contracts
|
|
}
|
|
|
|
func (e *EnvVarExtractor) extractCodeProviders(filePath string, src []byte) []Contract {
|
|
var contracts []Contract
|
|
text := string(src)
|
|
lines := strings.Split(text, "\n")
|
|
|
|
for _, re := range envProviderPatterns {
|
|
for _, m := range re.FindAllStringSubmatchIndex(text, -1) {
|
|
varName := text[m[2]:m[3]]
|
|
contracts = append(contracts, Contract{
|
|
ID: fmt.Sprintf("env::%s", varName),
|
|
Type: ContractEnv,
|
|
Role: RoleProvider,
|
|
FilePath: filePath,
|
|
Line: lineNumber(lines, m[0]),
|
|
Meta: map[string]any{"var": varName},
|
|
Confidence: 0.9,
|
|
})
|
|
}
|
|
}
|
|
|
|
return contracts
|
|
}
|
|
|
|
func (e *EnvVarExtractor) extractEnvFileProviders(filePath string, src []byte) []Contract {
|
|
var contracts []Contract
|
|
text := string(src)
|
|
lines := strings.Split(text, "\n")
|
|
|
|
for _, m := range envFileLineRe.FindAllStringSubmatchIndex(text, -1) {
|
|
varName := text[m[2]:m[3]]
|
|
contracts = append(contracts, Contract{
|
|
ID: fmt.Sprintf("env::%s", varName),
|
|
Type: ContractEnv,
|
|
Role: RoleProvider,
|
|
FilePath: filePath,
|
|
Line: lineNumber(lines, m[0]),
|
|
Meta: map[string]any{"var": varName, "source": "dotenv"},
|
|
Confidence: 0.95,
|
|
})
|
|
}
|
|
|
|
return contracts
|
|
}
|
|
|
|
func (e *EnvVarExtractor) extractDockerComposeProviders(filePath string, src []byte) []Contract {
|
|
var contracts []Contract
|
|
text := string(src)
|
|
lines := strings.Split(text, "\n")
|
|
|
|
// Only look within environment: sections.
|
|
envIdx := strings.Index(text, "environment:")
|
|
if envIdx < 0 {
|
|
return nil
|
|
}
|
|
|
|
section := text[envIdx:]
|
|
for _, m := range dockerEnvRe.FindAllStringSubmatchIndex(section, -1) {
|
|
varName := section[m[2]:m[3]]
|
|
contracts = append(contracts, Contract{
|
|
ID: fmt.Sprintf("env::%s", varName),
|
|
Type: ContractEnv,
|
|
Role: RoleProvider,
|
|
FilePath: filePath,
|
|
Line: lineNumber(lines, envIdx+m[0]),
|
|
Meta: map[string]any{"var": varName, "source": "docker-compose"},
|
|
Confidence: 0.9,
|
|
})
|
|
}
|
|
|
|
return contracts
|
|
}
|
|
|
|
func isEnvFile(path string) bool {
|
|
base := path
|
|
if idx := strings.LastIndex(path, "/"); idx >= 0 {
|
|
base = path[idx+1:]
|
|
}
|
|
return base == ".env" || strings.HasPrefix(base, ".env.")
|
|
}
|
|
|
|
func isDockerCompose(path string) bool {
|
|
base := path
|
|
if idx := strings.LastIndex(path, "/"); idx >= 0 {
|
|
base = path[idx+1:]
|
|
}
|
|
return strings.HasPrefix(base, "docker-compose") && (strings.HasSuffix(base, ".yml") || strings.HasSuffix(base, ".yaml"))
|
|
}
|