yellow~ opinionated web scanning pipeline for pentesters in a hurry |
| git clone https://github.com/djnnvx/yellow.git |
| log | files | refs | archive | README | LICENSE |
secrets.go (3753B)
1 package scan 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "io" 7 "net/http" 8 "os" 9 "strings" 10 "sync" 11 12 "evil.djnn.sh/djnn/yellow/core" 13 helper "evil.djnn.sh/djnn/yellow/helpers" 14 "github.com/zricethezav/gitleaks/v8/detect" 15 ) 16 17 const secretChunkKB = 1024 // gitleaks allocates 1000x this per in-flight fetch 18 19 type SecretFinding struct { 20 URL string `json:"url"` 21 RuleID string `json:"rule_id"` 22 Description string `json:"description"` 23 Secret string `json:"secret"` 24 Match string `json:"match"` 25 Line int `json:"line"` 26 Entropy float32 `json:"entropy"` 27 Fingerprint string `json:"fingerprint"` 28 } 29 30 type Secrets struct { 31 MaxURLs int 32 } 33 34 var ( 35 detectorOnce sync.Once 36 detector *detect.Detector 37 ) 38 39 func secretDetector() *detect.Detector { 40 detectorOnce.Do(func() { 41 d, err := detect.NewDetectorDefaultConfig() 42 if err != nil { 43 fmt.Printf("[!] secrets: could not load gitleaks rules: %v\n", err) 44 return 45 } 46 detector = d 47 }) 48 return detector 49 } 50 51 var scannableTypes = []string{ 52 "javascript", "json", "html", "css", "xml", "text/plain", "ecmascript", 53 } 54 55 func shouldScanBody(contentType string) bool { 56 ct := strings.ToLower(contentType) 57 for _, t := range scannableTypes { 58 if strings.Contains(ct, t) { 59 return true 60 } 61 } 62 return false 63 } 64 65 func (*Secrets) Name() string { return "secrets" } 66 67 func (s *Secrets) Run(ctx *core.Context) error { 68 fmt.Println("[+] Running secrets scan on", ctx.Domain) 69 if ctx.DryRun { 70 return nil 71 } 72 73 d := secretDetector() 74 if d == nil { 75 return nil 76 } 77 78 targets := capURLs(ctx.URLs, s.MaxURLs, "secrets") 79 if len(targets) == 0 { 80 targets = []string{ctx.Domain} 81 } 82 83 client := helper.GetHttpClient(true) 84 85 var ( 86 mu sync.Mutex 87 seen = map[string]bool{} 88 findings = []SecretFinding{} 89 scanned int 90 wg sync.WaitGroup 91 ) 92 sem := make(chan struct{}, 10) 93 94 for _, target := range targets { 95 wg.Add(1) 96 sem <- struct{}{} 97 go func(url string) { 98 defer wg.Done() 99 defer func() { <-sem }() 100 101 rc, ok := fetchScannable(client, url) 102 if !ok { 103 return 104 } 105 defer rc.Close() 106 107 mu.Lock() 108 scanned++ 109 mu.Unlock() 110 111 // gitleaks peeks to a safe boundary, so chunking loses nothing 112 found, err := d.DetectReader(rc, secretChunkKB) 113 if err != nil { 114 fmt.Printf("[!] secrets: %s: %v\n", url, err) 115 } 116 117 for _, f := range found { 118 rec := SecretFinding{ 119 URL: url, 120 RuleID: f.RuleID, 121 Description: f.Description, 122 Secret: f.Secret, 123 Match: strings.TrimSpace(f.Match), 124 Line: f.StartLine, 125 Entropy: f.Entropy, 126 Fingerprint: f.Fingerprint, 127 } 128 key := rec.Fingerprint 129 if key == "" { 130 key = rec.RuleID + ":" + rec.Secret 131 } 132 133 mu.Lock() 134 if !seen[key] { 135 seen[key] = true 136 findings = append(findings, rec) 137 fmt.Printf("[SCAN %s] secrets: [%s] %s in %s\n", ctx.Domain, rec.RuleID, rec.Description, url) 138 } 139 mu.Unlock() 140 } 141 }(target) 142 } 143 wg.Wait() 144 145 outfile := fmt.Sprintf("%s/secrets.json", ctx.ScanPath) 146 data, _ := json.MarshalIndent(findings, "", "\t") 147 if err := os.WriteFile(outfile, data, 0644); err != nil { 148 fmt.Printf("[!] secrets: could not write %s: %v\n", outfile, err) 149 } 150 151 fmt.Printf("[SCAN %s] secrets: %d finding(s) across %d URL(s) in %s\n\n", 152 ctx.Domain, len(findings), scanned, outfile) 153 return nil 154 } 155 156 func fetchScannable(client *http.Client, url string) (io.ReadCloser, bool) { 157 req, err := http.NewRequest("GET", url, nil) 158 if err != nil { 159 return nil, false 160 } 161 req.Header.Set("User-Agent", helper.GetUserAgent()) 162 163 resp, err := client.Do(req) 164 if err != nil { 165 return nil, false 166 } 167 168 if !shouldScanBody(resp.Header.Get("Content-Type")) { 169 resp.Body.Close() 170 return nil, false 171 } 172 173 return resp.Body, true 174 }