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 070c65c4d248cd49d9c05a9502d954d6f6550fb9
parent 9827698bed10356994e8e9d54ef06e050a0e4102
Author: djnn <[email protected]>
Date: Fri, 24 Oct 2025 21:59:46 +0200

feat(cmd): adding fingerprint subcommand

Diffstat:
MChangelog.md | 1+
MREADME.md | 5++---
Mcli/parser.go | 54+++++++++++++++++++++++++++++++++++++++++++++++++++++-
Ascan/fingerprint.go | 41+++++++++++++++++++++++++++++++++++++++++
Mscan/run.go | 15+++++----------

5 files changed, 102 insertions(+), 14 deletions(-)

diff --git a/Changelog.md b/Changelog.md
@@ -4,6 +4,7 @@
 - feat(scan): adding cvemap module ~ @djnnvx
 - fix(scan): adding --no-dirbusting ~ @djnnvx
 - feat(meta): adding run-web-recon script ~ @djnnvx
+- feat(cmd): adding fingerprint subcommand ~ @djnnvx
 
 
 ## v0.0.3
diff --git a/README.md b/README.md
@@ -1,5 +1,7 @@
 # yellow
 
+mirror available here: https://github.com/djnnvx/yellow
+
 ```
 
         ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣤⣤⣤⣤⣤⣤⣤⣄⣀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
@@ -44,10 +46,7 @@ Please use at your own risk, in a controlled environnement. Thanks<3
 
 For next version, i want to take care of at least two items described here:
 
-* `scan`: add toolbox scan script to automatically integrate it with
-          nmap and other CLI tools
 * `scan`: integrate browser-dependant tools (katana, ...) (still TBD),
-
 * `osint`: add support for more dorks
 * `osint`: shodan API integration
 
diff --git a/cli/parser.go b/cli/parser.go
@@ -14,6 +14,52 @@ import (
 
 func GetParser(opts *StandardOptions) *cobra.Command {
 
+	var fingerprintCmd = &cobra.Command{
+		Use:   "fingerprint",
+		Short: "Runs fingerprinting against websites",
+		Long:  "Runs active fingerprinting (wappalyzergo then cvemap)",
+		Args:  cobra.MinimumNArgs(0),
+		Run: func(cmd *cobra.Command, args []string) {
+
+			if opts.OutDirName == "" && opts.TargetFilePath == "" {
+				fmt.Println("[!] Error: target cannot be empty. Please run with --target [something] or --file [something]")
+				fmt.Println("\nIf you're confused, feel free to use --help option. :)~")
+				os.Exit(1)
+			}
+
+			helper.CheckProxy(opts.Proxy)
+			helper.DisplayNetInfo()
+
+			if opts.OutDirName == "" || !helper.Exists(opts.OutDirName) {
+				println("[!] collected assets will be sent to current working directory")
+			}
+
+			scanOpts := scan.ScanOpts{}
+			scanOpts.SetDomain(opts.OutDirName)
+			scanOpts.SetScanPath(opts.OutDirName)
+			scanOpts.SetProxy(opts.Proxy)
+			scanOpts.SetDryRun(opts.RunDry)
+			scanOpts.SetRateLimit(opts.RateLimit)
+			scanOpts.SetWordlistPath(opts.WordlistPath)
+			scanOpts.SetForceInsecure(opts.UseHttpInsecure)
+			scanOpts.SetNoGobuster(opts.NoGobuster)
+
+			if opts.TargetFilePath == "" {
+				scanOpts.Run()
+				return
+			}
+
+			scanner := helper.LoadTargetFile(opts.TargetFilePath)
+			defer scanner.Close()
+
+			for scanner.Scan() {
+				targetDomain := scanner.Text()
+				scanOpts.SetDomain(targetDomain)
+				scanOpts.Fingerprint()
+			}
+		},
+	}
+
 	var scanCmd = &cobra.Command{
 		Use:   "scan",
 		Short: "Run active scanning tools to perform enumeration",
@@ -149,11 +195,17 @@ func GetParser(opts *StandardOptions) *cobra.Command {
 	scanCmd.Flags().StringVarP(&opts.OutDirName, "dir-name", "d", defaults.OutDirName, "Outfile directory name (if no target-file is specified, will also be target domain)")
 	scanCmd.Flags().StringVarP(&opts.Proxy, "proxy", "p", defaults.Proxy, "Proxy URL (used for the tools supporting it. Other will prompt a warning msg)")
 	scanCmd.Flags().BoolVarP(&opts.RunDry, "dry", "", defaults.RunDry, "Run a dry-run (test mode)")
-	scanCmd.Flags().BoolVarP(&opts.NoGobuster, "no-dirbusting", "", defaults.NoGobuster, "Disable directory bruteforce (sometimes you don't need it yk...)")
+	scanCmd.Flags().BoolVarP(&opts.NoGobuster, "disable-dirbusting", "", defaults.NoGobuster, "Disable directory bruteforce (sometimes you don't need it yk...)")
 	scanCmd.Flags().BoolVarP(&opts.UseHttpInsecure, "insecure", "k", defaults.UseHttpInsecure, "Ignore SSL warnings and force http")
 	scanCmd.Flags().Int32VarP(&opts.RateLimit, "rate-limit", "r", defaults.RateLimit, "Requests rate-limit (used for the tools supporting it. Other will prompt a warning msg)")
 	scanCmd.Flags().StringVarP(&opts.WordlistPath, "wordlist", "w", defaults.WordlistPath, "Wordlist to use")
 	scanCmd.Flags().StringVarP(&opts.TargetFilePath, "file", "f", defaults.TargetFilePath, "File containing list of targets (should be a list of IP Addresses or domains)")
 
+	rootCmd.AddCommand(fingerprintCmd)
+	fingerprintCmd.Flags().StringVarP(&opts.OutDirName, "dir-name", "d", defaults.OutDirName, "Outfile directory name (if no target-file is specified, will also be target domain)")
+	fingerprintCmd.Flags().StringVarP(&opts.Proxy, "proxy", "p", defaults.Proxy, "Proxy URL (used for the tools supporting it. Other will prompt a warning msg)")
+	fingerprintCmd.Flags().BoolVarP(&opts.UseHttpInsecure, "insecure", "k", defaults.UseHttpInsecure, "Ignore SSL warnings and force http")
+	fingerprintCmd.Flags().StringVarP(&opts.TargetFilePath, "file", "f", defaults.TargetFilePath, "File containing list of targets (should be a list of IP Addresses or domains)")
+
 	return rootCmd
 }
diff --git a/scan/fingerprint.go b/scan/fingerprint.go
@@ -0,0 +1,41 @@
+package scan
+
+import (
+	"fmt"
+	"os"
+
+	helper "evil.djnn.sh/djnn/yellow/helpers"
+)
+
+/*
+   This is a sub-command, based on the scan command.
+
+   However, all it does is to run wappalyzer-go then cvemap on it
+   for quick wins.
+*/
+
+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) {
+		pathForDomain := opts.scanPath + "/" + opts.domain
+		opts.SetScanPath(pathForDomain)
+
+		err := os.MkdirAll(opts.scanPath, 0755)
+		if err != nil {
+			panic(err)
+		}
+
+		opts.SetDomain(fullDomain)
+
+		opts.RunWappalyzerGo()
+		opts.RunCvemap()
+	} else {
+		fmt.Printf("[FINGERPRINT %s] => no web panel online. skipping\n", opts.domain)
+	}
+}
diff --git a/scan/run.go b/scan/run.go
@@ -19,9 +19,7 @@ type ScanOpts struct {
 }
 
 /*
-	save in shared memory to avoid having to read a write from files
-
-all the time
+save in shared memory to avoid having to read a write from files all the time
 */
 var (
 	wappalyzerResults []string
@@ -73,7 +71,7 @@ func (opts ScanOpts) runRobots() {
 	}
 }
 
-func (opts ScanOpts) runWappalyzerGo() {
+func (opts ScanOpts) RunWappalyzerGo() {
 	wappalyzerResults = make([]string, 0)
 
 	wp := WappalyzerGo{}
@@ -89,7 +87,7 @@ func (opts ScanOpts) runWappalyzerGo() {
 	}
 }
 
-func (opts ScanOpts) runCvemap() {
+func (opts ScanOpts) RunCvemap() {
 	cv := Cvemap{}
 	cvCfg := make(map[string]any)
 
@@ -165,7 +163,6 @@ func (opts *ScanOpts) Run() {
 
 	if !helper.HasUnavailableWebInterface(fullDomain) {
 
-		initialPath := opts.scanPath
 		fmt.Printf("[SCAN %s] web panel online....running web scans\n", opts.domain)
 
 		pathForDomain := opts.scanPath + "/" + opts.domain
@@ -179,15 +176,13 @@ func (opts *ScanOpts) Run() {
 
 		opts.runSitemap()
 		opts.runRobots()
-		opts.runWappalyzerGo()
-		opts.runCvemap()
+		opts.RunWappalyzerGo()
+		opts.RunCvemap()
 		opts.runHttpx()
 
 		if !opts.noGoBuster {
 			opts.runGobusterDir()
 		}
-
-		opts.SetScanPath(initialPath)
 	} else {
 		fmt.Printf("[SCAN %s] => no web panel online. skipping\n", opts.domain)
 	}