yellow.git


yellow

~ opinionated web scanning pipeline for pentesters in a hurry
git clone https://github.com/djnnvx/yellow.git
log | files | refs | archive | README | LICENSE

commit 0faa59665d25cd14ed63f529f50577667b8b5de5
parent a908deb2f8bf40d7606f4ef8498ee670ce93d011
Author: djnn <[email protected]>
Date: Sun, 2 Aug 2026 14:09:54 +0900

fix(net): probe https then fall back to http, http-only hosts were skipped ~ @djnnvx

Diffstat:
MCHANGELOG.txt | 1+
Mhelpers/net.go | 19+++++++++++++++++++
Ahelpers/net_test.go | 55+++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mprune/run.go | 8++------
Mscan/fingerprint.go | 8++------
Mscan/run.go | 10+++-------

6 files changed, 82 insertions(+), 19 deletions(-)

diff --git a/CHANGELOG.txt b/CHANGELOG.txt
@@ -1,5 +1,6 @@
 ## v0.0.7
 
+- fix(net): probe https then fall back to http, http-only hosts were skipped ~ @djnnvx
 - fix(scan): open ports nerva cannot fingerprint no longer vanish from portscan.json ~ @djnnvx
 - chore(deps): bump dependencies ~ @djnnvx
 - refactor(core): dead code deletion, simplification ~ @djnnvx
diff --git a/helpers/net.go b/helpers/net.go
@@ -91,6 +91,25 @@ func CheckProxy(proxy string) {
 	}
 }
 
+// ResolveWebTarget probes https first and falls back to http, so a host that
+// only serves plaintext is still scanned instead of skipped.
+func ResolveWebTarget(domain string, forceInsecure bool) (string, bool) {
+	return resolveWebTarget(domain, forceInsecure, HasUnavailableWebInterface)
+}
+
+func resolveWebTarget(domain string, forceInsecure bool, unavailable func(string) bool) (string, bool) {
+	schemes := []string{"https://", "http://"}
+	if forceInsecure {
+		schemes = []string{"http://"}
+	}
+	for _, scheme := range schemes {
+		if target := scheme + domain; !unavailable(target) {
+			return target, true
+		}
+	}
+	return "", false
+}
+
 func HasUnavailableWebInterface(url string) bool {
 	url = strings.TrimSuffix(url, "/")
 
diff --git a/helpers/net_test.go b/helpers/net_test.go
@@ -0,0 +1,55 @@
+package helper
+
+import "testing"
+
+func TestResolveWebTargetFallsBackToHTTP(t *testing.T) {
+	probed := []string{}
+	unavailable := func(url string) bool {
+		probed = append(probed, url)
+		return url != "http://example.com"
+	}
+
+	got, ok := resolveWebTarget("example.com", false, unavailable)
+	if !ok {
+		t.Fatalf("host serving plaintext was skipped; probed %v", probed)
+	}
+	if got != "http://example.com" {
+		t.Errorf("got %q, want http://example.com", got)
+	}
+	if len(probed) != 2 || probed[0] != "https://example.com" {
+		t.Errorf("want https probed first then http, got %v", probed)
+	}
+}
+
+func TestResolveWebTargetPrefersHTTPS(t *testing.T) {
+	unavailable := func(string) bool { return false }
+
+	got, ok := resolveWebTarget("example.com", false, unavailable)
+	if !ok || got != "https://example.com" {
+		t.Errorf("got %q (ok=%v), want https://example.com", got, ok)
+	}
+}
+
+func TestResolveWebTargetForceInsecureSkipsHTTPS(t *testing.T) {
+	probed := []string{}
+	unavailable := func(url string) bool {
+		probed = append(probed, url)
+		return false
+	}
+
+	got, ok := resolveWebTarget("example.com", true, unavailable)
+	if !ok || got != "http://example.com" {
+		t.Errorf("got %q (ok=%v), want http://example.com", got, ok)
+	}
+	if len(probed) != 1 {
+		t.Errorf("force-insecure must not probe https, got %v", probed)
+	}
+}
+
+func TestResolveWebTargetDeadHost(t *testing.T) {
+	unavailable := func(string) bool { return true }
+
+	if got, ok := resolveWebTarget("example.com", false, unavailable); ok {
+		t.Errorf("dead host reported alive as %q", got)
+	}
+}
diff --git a/prune/run.go b/prune/run.go
@@ -27,13 +27,9 @@ func (opts *PruneOpts) Run() {
 
 	for scanner.Scan() {
 		targetDomain := scanner.Text()
-		httpAddr := "https://" + targetDomain
-		if opts.ForceInsecure {
-			httpAddr = "http://" + targetDomain
-		}
 
-		if !helper.HasUnavailableWebInterface(httpAddr) {
-			fmt.Printf("[+] %s is alive !\n", targetDomain)
+		if target, ok := helper.ResolveWebTarget(targetDomain, opts.ForceInsecure); ok {
+			fmt.Printf("[+] %s is alive (%s)!\n", targetDomain, target)
 			results = append(results, targetDomain)
 		}
 	}
diff --git a/scan/fingerprint.go b/scan/fingerprint.go
@@ -18,12 +18,8 @@ import (
 func (opts *ScanOpts) Fingerprint() {
 	fmt.Printf("\n[SCAN] domain: %s\n", opts.Domain)
 
-	fullDomain := "https://" + opts.Domain
-	if opts.ForceInsecure {
-		fullDomain = "http://" + opts.Domain
-	}
-
-	if helper.HasUnavailableWebInterface(fullDomain) {
+	fullDomain, ok := helper.ResolveWebTarget(opts.Domain, opts.ForceInsecure)
+	if !ok {
 		fmt.Printf("[FINGERPRINT %s] => no web panel online. skipping\n", opts.Domain)
 		return
 	}
diff --git a/scan/run.go b/scan/run.go
@@ -58,21 +58,17 @@ func (opts *ScanOpts) webModules() []core.Module {
 func (opts *ScanOpts) Run() {
 	fmt.Printf("\n[SCAN] domain: %s\n", opts.Domain)
 
-	fullDomain := "https://" + opts.Domain
-	if opts.ForceInsecure {
-		fullDomain = "http://" + opts.Domain
-	}
-
 	if opts.PortScan {
 		opts.runPortScan()
 	}
 
-	if helper.HasUnavailableWebInterface(fullDomain) {
+	fullDomain, ok := helper.ResolveWebTarget(opts.Domain, opts.ForceInsecure)
+	if !ok {
 		fmt.Printf("[SCAN %s] => no web panel online. skipping\n", opts.Domain)
 		return
 	}
 
-	fmt.Printf("[SCAN %s] web panel online....running web scans\n", opts.Domain)
+	fmt.Printf("[SCAN %s] web panel online (%s)....running web scans\n", opts.Domain, fullDomain)
 
 	/* per-domain output dir; setting opts.ScanPath directly would nest dirs */
 	pathForDomain := opts.ScanPath + "/" + opts.Domain